package ru.niir.goodfil.db.handlers;

import com.Ostermiller.util.RandPass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
import org.hibernate.Transaction;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.cfg.Configuration;
import ru.niir.goodfil.audit.client.AuditService;
import ru.niir.goodfil.audit.service.AuditServiceImpl;
import ru.niir.goodfil.audit.service.DefaultConfiguration;
import ru.niir.goodfil.audit.util.Assert;
import ru.niir.goodfil.audit.util.XMLUtils;
import ru.niir.goodfil.db.*;
import ru.niir.goodfil.db.audit.helpers.AuditHelper;
import ru.niir.goodfil.db.audit.helpers.NullAuditHelper;
import ru.niir.goodfil.db.dic.*;
import ru.niir.goodfil.db.handlers.hack.MyStatelessSession;
import ru.niir.goodfil.exceptions.*;
import ru.niir.goodfil.fun.ISSignature;
import ru.niir.goodfil.utils.*;
import ru.niir.goodfil.utils.filters.*;
import ru.niir.goodfil.utils.validation.CreateCheckResult;
import ru.niir.goodfil.utils.validation.RemoveCheckResult;
import ru.niir.goodfil.utils.validation.ValidationProblem;
import ru.niir.goodfil.utils.validation.ValidationResult;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.persistence.Entity;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.apache.commons.collections.CollectionUtils.collect;
import static org.apache.commons.lang.StringUtils.isBlank;
import static ru.niir.goodfil.utils.Utils.*;

/**
 * Центральный объект библиотеки GoodwillCore. Предназначен для выполнения операций с БД ИНТЕРНЕТ-системы.
 */
@SuppressWarnings({"unchecked"})
public class Goodfil implements IVariables {

    protected AuditService auditService = new AuditServiceImpl(new DefaultConfiguration());

    protected AuditService getAuditService() {
        return auditService;
    }

    private AuditHelper auditHelper = new NullAuditHelper();

    public AuditHelper getAuditHelper() {
        return auditHelper;
    }

    public void setAuditHelper(AuditHelper auditHelper) {
        this.auditHelper = auditHelper;
    }

    /**
     * Имя пользователя, которому принадлежит данный объект Goodfil (если он кому-то принадлежит)
     */
    private String username;

    /**
     * Дата логина пользователя, которому принадлежит данный объект Goodfil (если он кому-то принадлежит)
     */
    private Date loginDate;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Date getLoginDate() {
        return loginDate;
    }

    public void setLoginDate(Date loginDate) {
        this.loginDate = loginDate;
    }

    public Handler getHandler(Class klass) {
        Handler handler = handlers.get(klass);
        if (handler == null) {
            throw new NoHandlerException(klass);
        }

        handler.setAuditHelper(auditHelper);
        handler.setAuditService(auditService);
        return handler;
    }

    //
    //  section Goodfil extensions
    //  Общие (в смысле generic) методы для работы с сущностями
    //

    /**
     * Возвращает true для entity (в терминах hibernate) класса
     *
     * @param klass класс
     * @return true для entity (в терминах hibernate) класса
     */
    boolean isEntityClass(Class klass) {
        return klass.getAnnotation(Entity.class) != null;
    }

    /**
     * Возвращает true, если объект данного типа с заданным идентификатором присутствует в БД
     *
     * @param klass тип объекта
     * @param id    идентификатор
     * @return true, если объект данного типа с заданным идентификатором присутствует в БД
     * @throws NotEntityException если переданный класс не является сущностью (в терминах hibernate)
     */
    boolean exists(Class klass, long id) {
        if (!isEntityClass(klass)) {
            throw new NotEntityException(klass);
        }

        List result = getSession().createQuery("select id from " + klass.getSimpleName() + " where id=?").setLong(0, id).list();
        return result.size() > 0;
    }

    /**
     * Возвращает объект по его типу и идентификатору
     *
     * @param klass тип объекта
     * @param id    идентификатор
     * @return объект по его типу и идентификатору
     * @throws NotEntityException если переданный класс не является сущностью (в терминах hibernate)
     */
    Object get(Class klass, long id) {
        if (!isEntityClass(klass)) {
            throw new NotEntityException(klass);
        }

        synchronized (this) {
            return getSession().createQuery("from " + klass.getSimpleName() + " where id=?").setLong(0, id).uniqueResult();
        }
    }

    /**
     * Возвращает список объектов заданного типа, присутствующих в БД
     *
     * @param klass тип объекта
     * @return список объектов заданного типа, присутствующих в БД
     * @throws NotEntityException если переданный класс не является сущностью (в терминах hibernate)
     */
    List get(Class klass) {
        if (!isEntityClass(klass)) {
            throw new NotEntityException(klass);
        }

        synchronized (this) {
            return getSession().createQuery("from " + klass.getSimpleName()).list();
        }
    }

    /**
     * Выполяет проверку на уникальность некоторого поля внутри таблицы
     *
     * @param klass      тип объекта
     * @param fieldName  наименование поля (которое должно быть уникальным)
     * @param fieldValue значение поля (которое должно быть уникальным)
     * @param id         идентификатор записи, которую НЕ надо учитывать при проверке
     * @return true, если данное значение данного поля уникально внутри таблицы, не считая записи с идентификатором id
     */
    boolean isUnique(Class klass, String fieldName, Object fieldValue, long id) {
        if (!isEntityClass(klass)) {
            throw new NotEntityException(klass);
        }

        return count(klass, fieldName, fieldValue, id) == 1;
    }

    /**
     * Возвращает количество объектов, для которых fieldName == fieldValue  & id != id
     *
     * @param klass      тип объекта
     * @param fieldName  наименование поля
     * @param fieldValue значение поля
     * @param id         идентификатор объекта, который не учитывается при проверке
     * @return количество объектов
     */
    long count(Class klass, String fieldName, Object fieldValue, long id) {
        if (!isEntityClass(klass)) {
            throw new NotEntityException(klass);
        }

        synchronized (this) {
            return (Long) getSession().createQuery("select count(*) from " + klass.getSimpleName() + " where " + fieldName + "=? and id!=?").setParameter(0, fieldValue).setParameter(1, id).uniqueResult();
        }
    }

    /**
     * Функция добавления объекта в БД (INSERT). Функция: 1. Выбирает подходящий обработчик. 2. Производит проверку
     * состояния объекта (функция validate) 3. Производит проверку возможности создания объекта (функция createCheck) 4.
     * Вызывает событие preSave (метод обработчика) 5. Производит вставку записи в таблицу БД 6. Вызывает событие
     * postSave (метод обработчика) Операции 4-6 выполняются внутри транзакции.
     *
     * @param o объект
     * @throws ValidationException    в случае ошибок во внутренней структуре объекта
     * @throws CreateFailureException в случае невозможности создания объекта
     * @throws NoHandlerException     если для работы с данным типом не определен обработчик
     */
    private void save(Object o) throws ValidationException, CreateFailureException {
        Handler handler = getHandler(o.getClass());

        ValidationResult vresult = handler.validate(o);
        if (vresult.isError()) {
            throw new ValidationException(vresult);
        }

        if(!getIsReservedOn()){
            CreateCheckResult cresult = handler.createCheck(o);
            if (cresult.isError()) {
                throw new CreateFailureException(cresult);
            }
        }

        Transaction tx = getSession().beginTransaction();
        try {
            handler.preSave(o);
            getSession().insert(o);
            handler.postSave(o);
            tx.commit();

            handler.afterTransactionCommitOnSave(o);

/*            String objectXml = XMLUtils.getObjectXml(o);
            getAuditService().logEntityCreate(getAuditHelper().getIp(), getAuditHelper().getLogin(), o.getClass().getName(), objectXml);*/
        } finally {
            if (!tx.wasCommitted()) {
                logger.error("ROLLBACK ON SAVE");
                tx.rollback();
            }
        }
    }

    /**
     * Функция обновления данных об объекте в БД (UPDATE). 1. Выбирает подходящий обработчик. 2. Производит проверку
     * состояния объекта (функция validate) 3. Вызывает событие preUpdate (метод обработчика) 4. Производит обновление
     * записи таблицы БД 5. Вызывает событие postUpdate (метод обработчика) Операции 4-6 выполняются внутри транзакции.
     *
     * @param o объект
     * @throws ValidationException в случае ошибок во внутренней структуре объекта
     * @throws NoHandlerException  если для работы с данным типом не определен обработчик
     */
    private void update(Unique o) throws ValidationException {
        Handler handler = getHandler(o.getClass());

        ValidationResult vresult = handler.validate(o);
        if (vresult.isError()) {
            throw new ValidationException(vresult);
        }

        Transaction tx = getSession().beginTransaction();
        try {
            Object oldObject = getSession().get(o.getClass(), o.getId());
            oldObject = handler.fullLoadObject(oldObject);

            handler.preUpdate(o);
            getSession().update(o);
            handler.postUpdate(o);

            tx.commit();

            handler.afterTransactionCommitOnUpdate(oldObject, o);

            String objectChangeXml = XMLUtils.getObjectChangeXml(oldObject, o);
//            getAuditService().logEntityUpdate(getAuditHelper().getIp(), getAuditHelper().getLogin(), o.getClass().getName(), objectChangeXml);
        } finally {
            if (!tx.wasCommitted()) {
                logger.error("ROLLBACK ON UPDATE");
                tx.rollback();
            }
        }
    }

    private void update(Collection<? extends Unique> c) throws ValidationException {
        for (Unique o : c) {
            Handler handler = getHandler(o.getClass());

            ValidationResult vresult = handler.validate(o);
            if (vresult.isError()) {
                throw new ValidationException(vresult);
            }
        }

        List<Object[]> list = new ArrayList<Object[]>();
        Set<String> objectChangeXmls = new HashSet<String>();
        String objectClass = null;

        Transaction tx = getSession().beginTransaction();
        try {
            for (Unique o : c) {
                Handler handler = getHandler(o.getClass());
                objectClass = o.getClass().getName();

                Object oldObject = getSession().get(o.getClass(), o.getId());
                oldObject = handler.fullLoadObject(oldObject);
                String objectXml = XMLUtils.getObjectXml(oldObject);
                objectChangeXmls.add(objectXml);

                handler.preUpdate(o);
                getSession().update(o);
                handler.postUpdate(o);

                list.add(new Object[]{handler, oldObject, o});
            }

            tx.commit();

/*            for (String objectChangeXml : objectChangeXmls) {
                getAuditService().logEntityUpdate(getAuditHelper().getIp(),
                        getAuditHelper().getLogin(),
                        objectClass,
                        objectChangeXml
                );
            }*/

            for (Object[] objs : list) {
                Handler handler = (Handler) objs[0];
                handler.afterTransactionCommitOnUpdate(objs[1], objs[2]);
            }
        } finally {
            if (!tx.wasCommitted()) {
                logger.error("ROLLBACK ON UPDATE");
                tx.rollback();
            }
        }
    }

    /**
     * Функция удаления объекта из БД (DELETE). 1. Выбирает подходящий обработчик. 2. Проверяет, допустимо ли удаление
     * объекта (например по причине существования связанного с ним объектов) 3. Вызывает событие preRemove (метод
     * обработчика) 4. Производит удаление записи из таблицы БД
     *
     * @param klass тип объекта
     * @param id    идентификатор объекта
     * @throws RemoveFailureException если удаление объекта невозможно (например по причине существования связанного с
     *                                ним объектов)
     * @throws InvalidIdException     если объект с таким идентификатором отсутствует
     * @throws NoHandlerException     если для работы с данным типом не определен обработчик
     */
    private void remove(Class klass, long id) throws RemoveFailureException, InvalidIdException {
        Handler handler = getHandler(klass);

        RemoveCheckResult rresult = handler.removeCheck(id);
        if (rresult.isError()) {
            throw new RemoveFailureException(rresult);
        }

        Transaction tx = getSession().beginTransaction();
        try {
            Object oldObject = getSession().get(klass, id);
            String oldObjectXml = XMLUtils.getObjectXml(oldObject);

            handler.preRemove(id);
            getSession().createQuery("delete from " + klass.getSimpleName() + " where id=?").setLong(0, id).executeUpdate();
            tx.commit();

            handler.afterTransactionCommitOnRemove(oldObject);

/*            getAuditService().logEntityDelete(getAuditHelper().getIp(),
                    getAuditHelper().getLogin(),
                    klass.getName(),
                    oldObjectXml);*/
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    /**
     * Функция проверяет состояние объекта (REMOVE). Вызывается соответствующая функция обработчика.
     *
     * @param o объект
     * @return результат проверки состояния объекта
     * @throws NoHandlerException если для работы с данным типом не определен обработчик
     * @see ru.niir.goodfil.utils.validation.ValidationResult структура хранит результат проверки состояния объета
     * @see ru.niir.goodfil.db.handlers.Handler базовый класс для всех обработчиков
     */
    public ValidationResult validate(Object o) {
        Class klass = o.getClass();
        Handler handler = getHandler(o.getClass());

        return handler.validate(o);
    }

    /**
     * Функция выполняет проверку на возможность удаления объекта. Вызывается соответсвующая функция обработчика.
     *
     * @param klass тип объекта
     * @param id    идентификатор объекта
     * @return результат проверки на возможность удаления
     * @throws NoHandlerException если для работы с данным типом не определен обработчик
     * @see ru.niir.goodfil.utils.validation.RemoveCheckResult структура хранит результат проверки на возможность
     * удаления
     * @see ru.niir.goodfil.db.handlers.Handler базовый класс для всех обработчиков
     */
    public RemoveCheckResult removeCheck(Class klass, long id) throws InvalidIdException {
        Handler handler = getHandler(klass);

        return handler.removeCheck(id);
    }

    //
    //  section Обработчики
    //  Обработчики - это классы, содержащие логику, спецефическую для данной сущности.
    //  Например, для класса Good (товар) существует класс GoodHandler, содержащий, например, проверку на корректность
    //  состояния объекта Good. Все обработчики наследованы от класса Handler
    //

    /**
     * Обработчики
     */
    private final Map<Class, Handler> handlers = new HashMap<Class, Handler>();

    /**
     * Добавить обработчик
     *
     * @param handlerClass
     */
    private void registerHandler(Class handlerClass) throws InvocationTargetException, IllegalAccessException, InstantiationException {
        if (!handlerClass.getSuperclass().equals(Handler.class)) {   //  Если указанный хэндлер хэндлером не является
            throw new InvalidHandlerException(handlerClass);
        }

        Handler handler = (Handler) handlerClass.getConstructors()[0].newInstance(this);
        handlers.put(handler.getKlass(), handler);
    }


    //
    //  -------------------------------------------------------------------------------
    //

    private static Logger logger = Logger.getLogger(Goodfil.class);

    private static final SessionFactory factory;


    static {
        try {
            //
            //  Параметры БД
            //
            Configuration configuration = new AnnotationConfiguration().configure("hibernate.cfg.xml");
            factory = configuration.buildSessionFactory();

            //
            //  Протоколирование
            //
            System.out.println("Starting logging for goodwill core module");
            logger = Logger.getLogger(Goodfil.class);
            logger.debug("Logging ok for goodwill core module");
        } catch (Throwable ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }

    private StatelessSession session;

    private final Object pad = new Object();

    public StatelessSession getSession() {
        synchronized (pad) {
            if (this.session == null) {
                this.session = new MyStatelessSession(factory.openStatelessSession());
            }
            return this.session;
        }
    }

    public void relealse() {
        try {
            session.close();
        } catch (Exception e) {

        }
    }

    private static AtomicInteger instanceNumber = new AtomicInteger(0);

    public Goodfil() {
        this.auditService = auditService;

        logger.debug("Goodwill core module loaded: " + instanceNumber.incrementAndGet());

        for (int i = 0; i < 5; i++) {
            try {
                testConnection();
                break;
            } catch (Exception sqlExc) {
                logger.debug("Cannot establish connection with DB: " + sqlExc);
            }
        }

        getSession().createSQLQuery("set character_set_client=utf8").executeUpdate();
        getSession().createSQLQuery("set character_set_connection=utf8").executeUpdate();
        getSession().createSQLQuery("set character_set_results=utf8").executeUpdate();
        getSession().createSQLQuery("set collation_connection=utf8_general_ci").executeUpdate();

        Class[] handlers = new Class[]{
                ClientHandler.class,
                ClientGroupHandler.class,
                ClientHandler.class,
                GoodHandler.class,
                GoodTypeHandler.class,
                NotificationHandler.class,
                OrderHandler.class,
                OrderItemHandler.class,
                PersonalPriceHandler.class,
                ReplacementHandler.class,
                UserHandler.class,
                GoodAbsenceHandler.class,
                StorageDeliveryStatusHandler.class,
                StorageReturnHandler.class
        };

        for (int i = 0; i < handlers.length; i++) {
            try {
                registerHandler(handlers[i]);
            } catch (Exception e) {
                logger.fatal("Invalid handlers found: " + handlers[i].getName());
            }
        }
    }

    /**
     * Запрос для тестирования соединения
     */
    private void testConnection() {
        Object o = getSession().createQuery("select count(*) from Good").uniqueResult();
        logger.debug("Goods: " + o);
    }

    //
    //  -----------------------------------------------------------------------------------------------------------------------
    //

    //  Функции getById -------------------------------------------------------------------------------------------------------

//    private Set<Long> getClientgroupsByClientId(long clientId)
//    {
//        Set<Long> result = new HashSet<Long>();
//        List l = getSession().createSQLQuery("select IdClientGroup from `con_clients_clientgroups` where `IdClient` = ?").setLong(0, clientId).list();
//        Iterator i = l.iterator();
//        while (i.hasNext())
//        {
//            long clientGroupId = ((BigInteger) i.next()).longValue();
//            result.add(clientGroupId);
//        }
//        return result;
//    }

    public Good getGoodById(long id) {
        return (Good) get(Good.class, id);
    }

    public Good getGoodByCode(String code) {
        code = s(code);
        return (Good) getSession().createQuery("from Good where REPLACE(code, \' \', \'\')=?").setString(0, code.replaceAll(" ", "")).uniqueResult();
    }

    public void saveGood(Good good) throws ValidationException, CreateFailureException {
        save(good);
    }

    public synchronized void updateGood(Good good) throws ValidationException {
        update(good);
    }

    public synchronized void updateGoods(Collection<? extends Good> goods) throws ValidationException {
        update(goods);
    }

    public void updateGoodCount_Unsafe(String code, int count) {
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createSQLQuery("update `Goods` set `count`=? where `code`=?").setInteger(0, count).setString(1, code).executeUpdate();
            tx.commit();
        } catch (Exception e) {
            logger.error(e);
            e.printStackTrace();

            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    /**
     * Сгенерировать запрос для отбора товаров
     *
     * @param filter    фильтр для товаров
     * @param countOnly если true, то будут возвращены не элементы выборки, а их количество
     * @return запрос для отбора товаров
     */
    private Query generateGoodsQuery(GoodsFilter filter, boolean countOnly) {
        QueryBuilder b = countOnly ? QueryBuilder.WithWhere("select count(*) from Good g") : QueryBuilder.WithWhere("from Good g");
        if (filter != null) {
            if (!isBlank(filter.getCode())) {
                    //  Игнорирование пробелов при поиске по коду
                b.addWhereCondition("replace(g.code, ' ', '') like ?", filter.getCode().replaceAll(" ", "") + "%");
            }
            if (!isBlank(filter.getName())) {
                b.addWhereCondition("g.name like ?", "%" + filter.getName() + "%");
            }
            if (filter.getDeleted() != -1) {
                b.addWhereCondition("g.deleted=?", filter.getDeleted() != 0);
            }
            if (filter.getGoodTypeId() != -1) {
                b.addWhereCondition("g.goodTypeId = ?", filter.getGoodTypeId());
            }
            if (!isBlank(filter.getOe())) {
                b.addWhereCondition("g.oe like ?", "%" + filter.getOe() + "%");       //  Поиск по ое
            }
            if (filter.isOnlyDispose()) {
                b.addWhereCondition("g.count < g.dangerousRest");
            }
            if (filter.isOnlyWithComments()) {
                b.addWhereCondition("g.coment != ''");
            }
            if(filter.isOnlyDifferense()){
                b.addWhereCondition("g.count != g.storageCount");
            }
        }

        b.appendText("order by g.code asc");
        return b.toQuery(getSession());
    }

    public Query generateStorageEnteranceQuery(StorageEnteranceFilter enteranceFilter){
        QueryBuilder queryBuilder = QueryBuilder.WithWhere("from StorageEnterance s");
        if(enteranceFilter != null){
            if(enteranceFilter.getGoodId() != -1){
                queryBuilder.addWhereCondition("s.goodId = ?", enteranceFilter.getGoodId());
            }
            if(enteranceFilter.getBeginDate() != null){
                queryBuilder.addWhereCondition("DATE_FORMAT(s.date, '%Y%m%d') >= DATE_FORMAT(?, '%Y%m%d')", enteranceFilter.getBeginDate());
            }
            if(enteranceFilter.getEndDate() != null){
                queryBuilder.addWhereCondition("DATE_FORMAT(s.date, '%Y%m%d') <= DATE_FORMAT(?, '%Y%m%d')", enteranceFilter.getEndDate());
            }
            if(enteranceFilter.getOperationType() != -1){
                queryBuilder.addWhereCondition("s.operationType = ?", enteranceFilter.getOperationType());
            }
            if(enteranceFilter.getGoodTypeId() != -1){
                queryBuilder.addWhereCondition("s.goodTypeId = ?", enteranceFilter.getGoodTypeId());
            }
        }
        return queryBuilder.toQuery(getSession());
    }

    public List<StorageEnterance> getStorageEnteraces(StorageEnteranceFilter filter){
        return (List<StorageEnterance>)generateStorageEnteranceQuery(filter).list();
    }

    public long getGoodsCount(GoodsFilter filter) {
        synchronized (this) {
            return (Long) generateGoodsQuery(filter, true).uniqueResult();
        }
    }

    public long getGoodsCountWithout1C() {
        return (Long) getSession().createQuery("select count(*) from Good where c1code=?").setString(0, "").uniqueResult();
    }

    public List<Good> getBadGoods() {
        return getSession().createQuery("from Good where c1code=? order by code asc").setString(0, "").list();
    }

    public long getGoodsWithPricesCount2(long goodTypeId) {
        synchronized (this) {
            long l = ((BigInteger) getSession().createSQLQuery("select count(distinct(`IdGood`)) from `SimplePricelist` as sp, `Goods` as g where sp.`IdGood` = g.`Id` and LENGTH(g.`1CCode`) > 1 and g.`IdGoodType` = ?").setLong(0, goodTypeId).uniqueResult()).longValue();
            return l;
        }
    }

    public List<Good> getGoodsWithPrices(Set<Long> goodsIds) {
        List<Good> result = new ArrayList<Good>();

        String query = "select \n" +
                "g.*, \n" +
                "(select GROUP_CONCAT(CONCAT_WS('=', CONVERT(`IdClientGroup`, CHAR(10)), CONVERT(`Price`, CHAR(10))) SEPARATOR ' ') from `SimplePricelist` where `IdGood` = g.`Id`) as Prices\n" +
                "from Goods as g " +
                "where g.`Id` in (:goodsIds) " +
                "order by g.`Code` asc";
        List l = getSession().createSQLQuery(query).setParameterList("goodsIds", goodsIds).list();
        for (Object o : l) {
            Object[] arr = (Object[]) o;

            try {
                long id = ((BigInteger) arr[0]).longValue();
                long goodTypeId = ((BigInteger) arr[1]).longValue();
                boolean isDeleted = (Boolean) arr[2];
                String code = (String) arr[3];
                String c1code = (String) arr[4];
                String name = (String) arr[5];
                float minPrice = (Float) arr[6];
                float weight = (Float) arr[7];
                float volume = (Float) arr[8];
                String mh = (String) arr[9];
                String oe = (String) arr[10];
                String pr = (String) arr[11];
                int count = (Integer) arr[12];
                int reservedCount = (Integer) arr[13];
                String packageCount = (String) arr[14];
                String prices = (String) arr[16];

                Long brandId = null;
                Object oBrandId = arr[15];
                if (oBrandId != null) {
                    brandId = ((BigInteger) oBrandId).longValue();
                }


                Good good = new Good();
                good.setId(id);
                good.setGoodTypeId(goodTypeId);
                good.setDeleted(isDeleted);
                good.setCode(code);
                good.setC1code(c1code);
                good.setName(name);
                good.setMinPrice(minPrice);
                good.setWeight(weight);
                good.setVolume(volume);
                good.setMh(mh);
                good.setOe(oe);
                good.setPr(pr);
                good.setCount(count);
                good.setReservedCount(reservedCount);
                good.setPackageCount(packageCount);
                good.setBrandId(brandId);
                good.setPricesMapString(prices);

                result.add(good);
            } catch (NullPointerException npe) {
            }
        }

        return result;
    }

    public List<Good> getGoodsWithPrices(String goodCode) {
        List<Good> result = new ArrayList<Good>();

        String query = "select \n" +
                "g.*, \n" +
                "(select GROUP_CONCAT(CONCAT_WS('=', CONVERT(`IdClientGroup`, CHAR(10)), CONVERT(`Price`, CHAR(10))) SEPARATOR ' ') from `SimplePricelist` where `IdGood` = g.`Id`) as Prices\n" +
                "from Goods as g " +
                "where LENGTH(g.`1CCode`) > 1 and " +
                "    (g.`Oe` = ? or g.`Code` like ?) " +
                "order by g.`Code` desc;";
        List l = getSession().createSQLQuery(query).setString(0, goodCode).setString(1, "%" + goodCode + "%").list();
        for (Object o : l) {
            Object[] arr = (Object[]) o;

            try {
                long id = ((BigInteger) arr[0]).longValue();
                long goodTypeId = ((BigInteger) arr[1]).longValue();
                boolean isDeleted = (Boolean) arr[2];
                String code = (String) arr[3];
                String c1code = (String) arr[4];
                String name = (String) arr[5];
                float minPrice = (Float) arr[6];
                float weight = (Float) arr[7];
                float volume = (Float) arr[8];
                String mh = (String) arr[9];
                String oe = (String) arr[10];
                String pr = (String) arr[11];
                int count = (Integer) arr[12];
                int reservedCount = (Integer) arr[13];
                String packageCount = (String) arr[14];
                String prices = (String) arr[16];

                Long brandId = null;
                Object oBrandId = arr[15];
                if (oBrandId != null) {
                    brandId = ((BigInteger) oBrandId).longValue();
                }


                Good good = new Good();
                good.setId(id);
                good.setGoodTypeId(goodTypeId);
                good.setDeleted(isDeleted);
                good.setCode(code);
                good.setC1code(c1code);
                good.setName(name);
                good.setMinPrice(minPrice);
                good.setWeight(weight);
                good.setVolume(volume);
                good.setMh(mh);
                good.setOe(oe);
                good.setPr(pr);
                good.setCount(count);
                good.setReservedCount(reservedCount);
                good.setPackageCount(packageCount);
                good.setBrandId(brandId);
                good.setPricesMapString(prices);

                result.add(good);
            } catch (NullPointerException npe) {
            }
        }

        return result;
    }

    public List<Good> getGoodsWithPrices2(int first, int size, long goodTypeId1) {
        synchronized (this) {
            List<Good> result = new ArrayList<Good>();

            String query = "select \n" +
                    "g.*, \n" +
                    "(select GROUP_CONCAT(CONCAT_WS('=', CONVERT(`IdClientGroup`, CHAR(10)), CONVERT(`Price`, CHAR(10))) SEPARATOR ' ') from `SimplePricelist` where `IdGood` = g.`Id`) as Prices\n" +
                    "from Goods as g " +
                    "where LENGTH(g.`1CCode`) > 1 and g.`IdGoodType` = ? " +
                    "order by g.`Code` desc " +
                    "limit ?, ?";
            List l = getSession().createSQLQuery(query).setLong(0, goodTypeId1).setLong(1, first).setLong(2, size).list();
            for (Object o : l) {
                Object[] arr = (Object[]) o;

                try {
                    long id = ((BigInteger) arr[0]).longValue();
                    long goodTypeId = ((BigInteger) arr[1]).longValue();
                    boolean isDeleted = (Boolean) arr[2];
                    String code = (String) arr[3];
                    String c1code = (String) arr[4];
                    String name = (String) arr[5];
                    float minPrice = (Float) arr[6];
                    float weight = (Float) arr[7];
                    float volume = (Float) arr[8];
                    String mh = (String) arr[9];
                    String oe = (String) arr[10];
                    String pr = (String) arr[11];
                    int count = (Integer) arr[12];
                    int reservedCount = (Integer) arr[13];
                    String packageCount = (String) arr[14];
                    int dangerousRest = (Integer) arr[16];
                    String coment = (String) arr[17];
                    int storageCount = (Integer) arr[18];
                    String prices = (String) arr[19];

                    Long brandId = null;
                    Object oBrandId = arr[15];
                    if (oBrandId != null) {
                        brandId = ((BigInteger) oBrandId).longValue();
                    }


                    Good good = new Good();
                    good.setId(id);
                    good.setGoodTypeId(goodTypeId);
                    good.setDeleted(isDeleted);
                    good.setCode(code);
                    good.setC1code(c1code);
                    good.setName(name);
                    good.setMinPrice(minPrice);
                    good.setWeight(weight);
                    good.setVolume(volume);
                    good.setMh(mh);
                    good.setOe(oe);
                    good.setPr(pr);
                    good.setCount(count);
                    good.setReservedCount(reservedCount);
                    good.setPackageCount(packageCount);
                    good.setBrandId(brandId);
                    good.setPricesMapString(prices);
                    good.setDangerousRest(dangerousRest);
                    good.setComent(coment);
                    good.setStorageCount(storageCount);
                    result.add(good);
                } catch (NullPointerException npe) {
                }
            }

            return result;
        }
    }

    public List<Good> getGoods(int first, int size, GoodsFilter filter) {
        if (first < 0) {
            throw new IllegalArgumentException("Value of `first` should be positive");
        }
        if (size < 0) {
            throw new IllegalArgumentException("Value of `size` should be positive");
        }

        return (List<Good>) generateGoodsQuery(filter, false).setFirstResult(first).setMaxResults(size).list();
    }

    public List<Good> getGoods(GoodsFilter filter) {
        return (List<Good>) generateGoodsQuery(filter, false).list();
    }

    public List<Long> getGoodsIds(int first, int size, GoodsFilter filter) {
        QueryBuilder b = QueryBuilder.WithWhere("select g.id from Good g");
        if (filter != null) {
            b.addWhereCondition("g.code like ?", "%" + filter.getCode() + "%");
            b.addWhereCondition("g.name like ?", "%" + filter.getName() + "%");
            if (filter.getDeleted() != -1) {
                b.addWhereCondition("g.deleted=?", filter.getDeleted() != 0);
            }
            if (filter.getGoodTypeId() != -1) {
                b.addWhereCondition("g.goodTypeId = ?", filter.getGoodTypeId());
            }
            if (!isBlank(filter.getOe())) {
                b.addWhereCondition("g.oe like ?", "%" + filter.getOe() + "%");       //  Поиск по ое
            }
            if (filter.isOnlyDispose()) {
                b.addWhereCondition("g.count < g.dangerousRest");
            }
        }

        return (List<Long>) b.toQuery(getSession()).setFirstResult(first).setMaxResults(size).list();
    }

    public boolean isGoodCodeUnique(String code, long id) {
        return !isUnique(Good.class, "code", code, id);
    }

    public void softRemoveGoodById(long id) throws InvalidIdException {
        if (!exists(Good.class, id)) {
            throw new InvalidIdException(Good.class, id);
        }

        Transaction tx = getSession().beginTransaction();
        getSession().createQuery("update Good set isDeleted=true where id=?").setLong(0, id).executeUpdate();
        tx.commit();
    }

    public void softUnRemoveGoodById(long id) throws InvalidIdException {
        if (!exists(Good.class, id)) {
            throw new InvalidIdException(Good.class, id);
        }

        Transaction tx = getSession().beginTransaction();
        getSession().createQuery("update Good set isDeleted=false where id=?").setLong(0, id).executeUpdate();
        tx.commit();
    }

    public List<Good> getGoodsBy1CCode(String c1Code) {
        if (c1Code != null) {
            c1Code = c1Code.trim();
        }
        return (List<Good>) getSession().createQuery("from Good where c1code=?").setString(0, c1Code).list();
    }

    /**
     * Дополнить карточку клиента информацией об относящихся к нему колонках цен
     */
    void client_appendClientgroups(Client client) {
        _assertNotNull(client);

        //  Дополняем объект информацией о клиентских группах
        for (Brand brand : getBrands()) {
            long clientGroupId = getClientGroupByClientId2(client.getId(), brand.getId());
            client.getClientgroups().put(brand.getId(), clientGroupId);
        }
    }

    /**
     * Дополнить карточку клиента информацией о видимых для него колонках цен
     */
    void client_appendVisibleClientgroups(Client client) {
        _assertNotNull(client);

        client.initVisibleClientGroups();

        //  Дополняем объект информацией о видимых колонках цен
        List l = getSession().createSQLQuery("select `IdClientGroup` FROM `ConClientsClientgroupsVisible` where `IdClient` = ?")
                .setLong(0, client.getId())
                .list();
        Iterator i = l.iterator();
        while (i.hasNext()) {
            long clientGroupId = ((BigInteger) i.next()).longValue();
            client.getVisibleClientGroups().add(clientGroupId);
        }
    }

    /**
     * Дополнить карточку клиента информацией о видимых для него брэндах
     */
    void client_appendVisibleBrands(Client client) {
        _assertNotNull(client);

        client.initVisibleBrands();

        //  Дополняем объект информацией о видимых брэндах
        List l = getSession().createSQLQuery("select `IdBrand` FROM `ConClientsBrandsVisible` where `IdClient` = ?")
                .setLong(0, client.getId())
                .list();
        Iterator i = l.iterator();
        while (i.hasNext()) {
            long brandId = ((BigInteger) i.next()).longValue();
            client.getVisibleBrands().add(brandId);
        }
    }

    /**
     * Дополнить карточку клиента расширенной информацией о доставке
     */
    void client_appendDeliveryInfo(Client client) {
        _assertNotNull(client);

        client.setDeliveryInfoExt(clientsDeliveryInfo.getClientDeliveryInfoById(client.getId()));
    }

    /**
     * Возвращает запись клиента по идентификатору. Или null если запись с указанным идентификатором отсутствует.
     */
    public Client getClientById(long id) {
        Client client = (Client) get(Client.class, id);

        if (client != null) {
            client_appendClientgroups(client);              //  Дополняем объект информацией о клиентских группах
            client_appendVisibleClientgroups(client);       //  Дополняем объект информацией о видимых колонках цен
            client_appendVisibleBrands(client);             //  Дополняем объект информацией о видимых брэндах
            client_appendDeliveryInfo(client);              //  Дополняем объект расширенной информацией о доставке
        }

        return client;
    }

    public Client getClientBy1CCode(String c1code) {
        _assertNotBlank(c1code);

        Client client = (Client) getSession().createQuery("from Client where c1ClientNum=?").setString(0, c1code).uniqueResult();

        if (client != null) {
            client_appendClientgroups(client);              //  Дополняем объект информацией о клиентских группах
            client_appendVisibleClientgroups(client);       //  Дополняем объект информацией о видимых колонках цен
            client_appendVisibleBrands(client);             //  Дополняем объект информацией о видимых брэндах
            client_appendDeliveryInfo(client);              //  Дополняем объект расширенной информацией о доставке
        }

        return client;
    }

    /**
     * Возвращает запись пользователя по идентификатору. Или null если запись с указанным идентификатором отсутствует.
     */
    public User getUserById(long id) {
        return (User) get(User.class, id);
    }

    /**
     * Возвращает учетную запись администратора системы.
     */
    public User getAdminUser() {
        return getUserById(1);
    }

    /**
     * Возвращает запись заказа по идентификатору. Или null если запись с указанным идентификатором отсутствует.
     */
    public Order getOrderById(long id) {
        Order order = (Order) get(Order.class, id);
        if (order != null) {
            order.setDeliveryInfoExt(clientsDeliveryInfo.getOrderDeliveryInfoById(id));
        }
        return order;
    }

    /**
     * Возвращает последний (по дате создания), неудаленный заказ в статусе "предварительно сохранен" для указанного
     * клиента.
     *
     * @param clientId идентификатор клиента
     */
    public Order getLastPresavedOrder(long clientId) {
        synchronized (this) {
            List<Order> orders = getSession().createQuery("from Order where clientId = ? and isDeleted = false and orderStateId = ? order by creationDate desc")
                    .setLong(0, clientId)
                    .setInteger(1, OrderState.PRE_SAVED)
                    .setMaxResults(1)
                    .list();
            return orders.size() > 0 ? orders.get(0) : null;
        }
    }

    /**
     * Возвращает запись позиции заказа по идентификатору. Или null если запись с указанным идентификатором
     * отсутствует.
     */
    public OrderItem getOrderItemById(long id) {
        return (OrderItem) get(OrderItem.class, id);
    }

    /**
     * Возвращает запись группы товаров по идентификатору. Или null если запись с указанным идентификатором
     * отсутствует.
     */
    public GoodType getGoodTypeById(long id) {
        return (GoodType) get(GoodType.class, id);
    }

    /**
     * Возвращает запись клиентской группы по идентификатору. Или null если запись с указанным идентификатором
     * отсутствует.
     */
    public ClientGroup getClientGroupById(long id) {
        return (ClientGroup) get(ClientGroup.class, id);
    }

    /**
     * Возвращает запись персональной цены по идентификатору. Или null если запись с указанным идентификатором
     * отсутствует.
     */
    public PersonalPrice getPersonalPriceById(long id) {
        return (PersonalPrice) get(PersonalPrice.class, id);
    }

    /**
     * Возвращает запись замещения по идентификатору. Или null если запись с указанным идентификатором отсутствует.
     */
    public Replacement getReplacementById(long id) {
        return (Replacement) get(Replacement.class, id);
    }

    //  ------------------------------------------------------------------------------------------------------------------------

    //  Функции remove ---------------------------------------------------------------------------------------------------------

    /**
     * Удалить клиента по идентификатору.
     *
     * @param id идентификатор
     * @throws RemoveFailureException в случае невозможности удаления
     * @throws InvalidIdException     в случае, если запись с указанным идентификатором отсутствует
     */
    public void removeClient(long id) throws RemoveFailureException, InvalidIdException {
        remove(Client.class, id);
    }

    /**
     * Удалить пользователя по идентификатору.
     *
     * @param id идентификатор
     * @throws RemoveFailureException в случае невозможности удаления
     * @throws InvalidIdException     в случае, если запись с указанным идентификатором отсутствует
     */
    public void removeUser(long id) throws RemoveFailureException, InvalidIdException {
        remove(User.class, id);
    }

    /**
     * Удалить товар по идентификатору.
     *
     * @param id идентификатор
     * @throws RemoveFailureException в случае невозможности удаления
     * @throws InvalidIdException     в случае, если запись с указанным идентификатором отсутствует
     */
    public void removeGood(long id) throws RemoveFailureException, InvalidIdException {
        remove(Good.class, id);
    }

    /**
     * Удалить группу товаров по идентификатору.
     *
     * @param id идентификатор
     * @throws RemoveFailureException в случае невозможности удаления
     * @throws InvalidIdException     в случае, если запись с указанным идентификатором отсутствует
     */
    public void removeGoodType(long id) throws RemoveFailureException, InvalidIdException {
        remove(GoodType.class, id);
    }

    /**
     * Удалить персональную цену по идентификатору.
     *
     * @param id идентификатор
     * @throws RemoveFailureException в случае невозможности удаления
     * @throws InvalidIdException     в случае, если запись с указанным идентификатором отсутствует
     */
    public void removePersonalPrice(long id) throws RemoveFailureException, InvalidIdException {
        remove(PersonalPrice.class, id);
    }

    /**
     * Удалить заказ по идентификатору.
     *
     * @param id идентификатор
     * @throws RemoveFailureException в случае невозможности удаления
     * @throws InvalidIdException     в случае, если запись с указанным идентификатором отсутствует
     */
    public void removeOrder(long id) throws RemoveFailureException, InvalidIdException {
        remove(Order.class, id);
    }

    /**
     * Удалить позицию заказа по идентификатору.
     *
     * @param id идентификатор
     * @throws RemoveFailureException в случае невозможности удаления
     * @throws InvalidIdException     в случае, если запись с указанным идентификатором отсутствует
     */
    public void removeOrderItem(long id) throws RemoveFailureException, InvalidIdException {
        remove(OrderItem.class, id);
    }

    /**
     * Удалить клиентскую группу по идентификатору.
     *
     * @param id идентификатор
     * @throws RemoveFailureException в случае невозможности удаления
     * @throws InvalidIdException     в случае, если запись с указанным идентификатором отсутствует
     */
    public void removeClientGroup(long id) throws RemoveFailureException, InvalidIdException {
        remove(ClientGroup.class, id);
    }

    public void removeReplacement(long id) throws RemoveFailureException, InvalidIdException {
        remove(Replacement.class, id);
    }

    //  ------------------------------------------------------------------------------------------------------------------------

    //  Функции save -----------------------------------------------------------------------------------------------------------


    /**
     * Добавить заказ в БД
     *
     * @param order заказ
     * @throws ValidationException    в случае ошибок внутри объекта
     * @throws CreateFailureException в случае невозможности создани объекта
     */
    public void saveOrder(Order order) throws ValidationException, CreateFailureException {
        save(order);
    }

    /**
     * Добавить группа товаров в БД
     *
     * @param goodType группа товаров
     * @throws ValidationException    в случае ошибок внутри объекта
     * @throws CreateFailureException в случае невозможности создани объекта
     */
    public void saveGoodType(GoodType goodType) throws ValidationException, CreateFailureException {
        save(goodType);
    }

    /**
     * Добавить клиента в БД
     *
     * @param client клиент
     * @throws ValidationException    в случае ошибок внутри объекта
     * @throws CreateFailureException в случае невозможности создани объекта
     */
    public void saveClient(Client client) throws ValidationException, CreateFailureException {
        save(client);
    }

    /**
     * Добавить пользователя в БД
     *
     * @param user пользователь
     * @throws ValidationException    в случае ошибок внутри объекта
     * @throws CreateFailureException в случае невозможности создани объекта
     */
    public void saveUser(User user) throws ValidationException, CreateFailureException {
        save(user);
    }

    /**
     * Добавить клиентскую группу в БД
     *
     * @param clientGroup клиентская группа
     * @throws ValidationException    в случае ошибок внутри объекта
     * @throws CreateFailureException в случае невозможности создани объекта
     */
    public void saveClientGroup(ClientGroup clientGroup) throws ValidationException, CreateFailureException {
        save(clientGroup);
    }

    /**
     * Добавить позицию заказа в БД
     *
     * @param orderItem позиция заказа
     * @throws ValidationException    в случае ошибок внутри объекта
     * @throws CreateFailureException в случае невозможности создани объекта
     */
    public void saveOrderItem(OrderItem orderItem) throws ValidationException, CreateFailureException {
        save(orderItem);
    }

    /**
     * Добавить персональную цену в БД
     *
     * @param personalPrice персональная цена
     * @throws ValidationException    в случае ошибок внутри объекта
     * @throws CreateFailureException в случае невозможности создани объекта
     */
    public void savePersonalPrice(PersonalPrice personalPrice) throws ValidationException, CreateFailureException {
        save(personalPrice);
    }

    /**
     * Добавить замещение в БД
     *
     * @param replacement замещение
     * @throws ValidationException    в случае ошибок внутри объекта
     * @throws CreateFailureException в случае невозможности создани объекта
     */
    public void saveReplacement(Replacement replacement) throws ValidationException, CreateFailureException {
        save(replacement);
    }

    /**
     *
     * @param storageReturn
     * @throws ValidationException
     * @throws CreateFailureException
     */
    public void saveStorageReturn(StorageReturn storageReturn) throws ValidationException, CreateFailureException {
        save(storageReturn);
    }

    //  ------------------------------------------------------------------------------------------------------------------------

    //  Функции update ---------------------------------------------------------------------------------------------------------


    /**
     * Обновить группа товаров в БД
     *
     * @param goodType группа товаров
     * @throws ValidationException в случае ошибок внутри обновляемого объекта
     */
    public void updateGoodType(GoodType goodType) throws ValidationException {
        update(goodType);
    }

    /**
     * Обновить заказ в БД
     *
     * @param order заказ
     * @throws ValidationException в случае ошибок внутри обновляемого объекта
     */
    public void updateOrder(Order order) throws ValidationException {
        update(order);
    }

//    /**
//     * Обновить запись клиента в БД
//     * COMMENT
//     * @param client клент
//     * @throws ValidationException в случае ошибок внутри обновляемого объекта
//     */
//    public void updateClient(Client client, boolean useHandler) throws ValidationException
//    {
//        _assert(client != null);
//
//        if (useHandler)
//        {
//            update(client);
//        }
//        else
//        {
//            Transaction tx = getSession().beginTransaction();
//            try
//            {
//                logger.debug("Updating client without using handler");
//                getSession().update(client);
//                ClientHandler.updateClientgroupsInfo(getSession(), client);
//                ClientHandler.updateVisibleBrandsInfo(getSession(), client);
//                ClientHandler.updateVisibleClientgroupsInfo(getSession(), client);
//                ClientHandler.insertOrUpdateClientDeliveryInfoExt(getSession(), client);
//                tx.commit();
//            }
//            finally
//            {
//                if (!tx.wasCommitted()) tx.rollback();
//            }
//        }
//    }

    /**
     * Обновить запись клиента в БД
     *
     * @param client клент
     * @throws ValidationException в случае ошибок внутри обновляемого объекта
     */
    public void updateClient(Client client) throws ValidationException {
        _assert(client != null);

        update(client);
    }

    /**
     * Обновить пользователь в БД
     *
     * @param user пользователь
     * @throws ValidationException в случае ошибок внутри обновляемого объекта
     */
    public void updateUser(User user) throws ValidationException {
        update(user);
    }

    /**
     * Обновить клиентская группа в БД
     *
     * @param clientGroup клиентская группа
     * @throws ValidationException в случае ошибок внутри обновляемого объекта
     */
    public void updateClientGroup(ClientGroup clientGroup) throws ValidationException {
        update(clientGroup);
    }

    /**
     * Обновить позиция заказа в БД
     *
     * @param orderItem позиция заказа
     * @throws ValidationException в случае ошибок внутри обновляемого объекта
     */
    public void updateOrderItem(OrderItem orderItem) throws ValidationException {
        update(orderItem);
    }

    public void updateOrderItems(List<OrderItem> orderItems) throws ValidationException {
        update(orderItems);
    }

    /**
     * Обновить персональные цены в БД
     *
     * @param personalPrice персональные цены
     * @throws ValidationException в случае ошибок внутри обновляемого объекта
     */
    public void updatePersonalPrice(PersonalPrice personalPrice) throws ValidationException {
        update(personalPrice);
    }

    /**
     * Обновить замещение в БД
     *
     * @param replacement замещение
     * @throws ValidationException в случае ошибок внутри обновляемого объекта
     */
    public void updateReplacement(Replacement replacement) throws ValidationException {
        update(replacement);
    }

    /**
     * Восстановление блокировок. Всем не архивным клиентам IsCheckBlock true
     */
    public void blockRecovery(){
        getSession().createSQLQuery("update clients set IsBlockCheck = 1 where IsActive = 1").executeUpdate();
    }

    /**
     * Обновление элемента для листа развоза
     *
     * @param status элемент листа развоза
     * @throws ValidationException
     */
    public synchronized void updateStorageDeliveryStatus(StorageDeliveryStatus status) throws ValidationException {
        Calendar now = Calendar.getInstance();
        now.setTime(status.getDate());
        now.set(Calendar.HOUR_OF_DAY, 0);
        now.set(Calendar.MINUTE, 0);
        now.set(Calendar.SECOND, 0);
        now.set(Calendar.MILLISECOND, 0);
        if (now.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY) {
            now.add(Calendar.DATE, 3);
        } else if (now.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
            now.add(Calendar.DATE, 2);
        } else {
            now.add(Calendar.DATE, 1);
        }

        List<StorageDeliveryStatus> deliveryStatusList = getPeriodicStoragedeliveryStatusByOrderIdAndDate(status.getIdOrder(), new Date(status.getDate().getTime() + TimeUnit.DAYS.toMillis(1)));
        if (deliveryStatusList.size() == 0) {
            if (status.isDeliveryStateIsNotDelivery()) {
                saveStorageDeliveryStatus(new StorageDeliveryStatus(status.getIdOrder(), now.getTime(), status.getIdDeliveryType(), status.getClientStateId()));
            }
        }

        update(status);
    }

    //  ------------------------------------------------------------------------------------------------------------------------

    //  Функции removeCheck ----------------------------------------------------------------------------------------------------

    /**
     * Проверка на возможность удаления товара
     *
     * @param id идентификатор товара
     * @return результат проверки
     * @throws InvalidIdException неверный идентификатор объекта
     */
    public RemoveCheckResult removeCheckGood(long id) throws InvalidIdException {
        return removeCheck(Good.class, id);
    }

    /**
     * Проверка на возможность удаления клиенсткой группы
     *
     * @param id идентификатор клиенсткой группы
     * @return результат проверки
     * @throws InvalidIdException неверный идентификатор объекта
     */
    public RemoveCheckResult removeCheckClientGroup(long id) throws InvalidIdException {
        return removeCheck(ClientGroup.class, id);
    }

    /**
     * Проверка на возможность удаления пользователя
     *
     * @param id идентификатор пользователя
     * @return результат проверки
     * @throws InvalidIdException неверный идентификатор объекта
     */
    public RemoveCheckResult removeCheckUser(long id) throws InvalidIdException {
        return removeCheck(User.class, id);
    }

    /**
     * Проверка на возможность удаления замещения
     *
     * @param id идентификатор замещения
     * @return результат проверки
     * @throws InvalidIdException неверный идентификатор объекта
     */
    public RemoveCheckResult removeCheckReplacement(long id) throws InvalidIdException {
        return removeCheck(Replacement.class, id);
    }

    //  ------------------------------------------------------------------------------------------------------------------------

    //
    //  section System variables ok
    //

    /**
     * Наименование настройки, в которой хранится идентификатор корневой группы товаров ("ВСЕ ТОВАРЫ")
     */
    private static final String GOODTYPES_ROOT_ID = ".goodtypes.root.id";

    /**
     * Возвращает идентификатор корневой группы товаров из настроек
     *
     * @return идентификатор корневой группы товаров
     * @throws SettingNotFoundException если в БД отсутствует необходимая настройка (Setting)
     */
    private int getRootGoodTypeId() throws SettingNotFoundException {
        createIfNotExists(GOODTYPES_ROOT_ID, 0);
        return getInt(GOODTYPES_ROOT_ID);
    }

    //
    //  section Settings ok
    //

    /**
     * Возвращает перечень настроек приложения.
     */
    public synchronized List<Setting> getSettings() {
        return (List<Setting>) get(Setting.class);
    }

    /**
     * Получить настройку по имени.
     */
    private synchronized Setting getSettingByName(String key) {
        _assertNotBlank(key);

        synchronized (this) {
            return (Setting) getSession().createQuery("from Setting where key=?").setString(0, key).uniqueResult();
        }
    }

    /**
     * Установить опцию. Опция будет создана, если опции с указанным именем не существует.
     *
     * @param key   наименование опции
     * @param value значение опции
     */
    public synchronized void set(String key, String value) {
        assert key != null;
        assert value != null;

        key = key.trim();
        value = value.trim();

        if (isBlank(key) || isBlank(value)) {
            return;
        }

        Setting setting = getSettingByName(key);
        if (setting == null) {
            Setting s = new Setting(key, value);
            logger.debug("inserting setting " + s);
            Transaction tx = getSession().beginTransaction();
            getSession().insert(s);
            tx.commit();

/*            getAuditService().logSettingModification(getAuditHelper().getIp(),
                    getAuditHelper().getLogin(),
                    key,
                    value,
                    null);*/

        } else {
            String oldValue = setting.getValue();
            setting.setValue(value);
            logger.debug("updating setting " + setting);
            Transaction tx = getSession().beginTransaction();
            getSession().update(setting);
            tx.commit();

/*            getAuditService().logSettingModification(getAuditHelper().getIp(),
                    getAuditHelper().getLogin(),
                    key,
                    value,
                    oldValue);*/
        }
    }

    /**
     * Получить значение опции по ее наименованию. Если опции с таким именем не существует будет возвращен null
     */
    public synchronized String get(String key) {
        assert key != null;

        Setting setting = getSettingByName(key);
        return (setting == null) ? null : setting.getValue();
    }

    /**
     * Получить значение опции по ее наименованию как integer Если опции с таким именем не существует или она не
     * приводима к integer'у будет возвращен null
     */
    public synchronized Integer getInt(String key) {
        assert key != null;

        try {
            return Integer.parseInt(get(key));
        } catch (NumberFormatException e) {
            return null;
        }
    }

    /**
     * Создать опцию в БД если таковая отсутствует. Если опция с указанным именем уже существует, ее значение НЕ будет
     * изменено
     *
     * @param key   наименование опции
     * @param value значение опции
     * @deprecated Следует использовать метод с таким же названием, но с 3мя аргументами. 3ый аргумент - комментарий к
     * настройке.
     */
    @Deprecated
    public synchronized void createIfNotExists(String key, String value) {
        assert key != null;
        assert value != null;

        if (get(key) == null) {
            set(key, value);
        }
    }

    /**
     * Создать опцию в БД если таковая отсутствует. Если опция с указанным именем уже существует, ее значение НЕ будет
     * изменено
     *
     * @param key     наименование опции
     * @param value   значение опции
     * @param comment описание опции на русском языке
     */
    public synchronized void createIfNotExists(String key, String value, String comment) {
        _assertNotBlank(key);
        _assertNotBlank(value);
        _assertNotBlank(comment);

        Setting setting = (Setting) getSession().createQuery("from Setting where key=?").setString(0, key).uniqueResult();

        if (setting == null) {   //  Создание новой опции
            Transaction tx = getSession().beginTransaction();
            getSession().insert(new Setting(key, value, comment));
            tx.commit();
        }
    }

    /**
     * Получить строковое значение настройки или создать такую настройку, если она отсутствует
     *
     * @param key     имя настройки
     * @param value   значение настройки
     * @param comment наименование настройки на русском
     * @return значение настройки (существующей или только что созданной)
     */
    public synchronized String getOrCreateIfNotExists(String key, String value, String comment) {
        _assertNotBlank(key);
        _assertNotBlank(value);
        _assertNotBlank(comment);

        Setting setting = (Setting) getSession().createQuery("from Setting where key=?").setString(0, key).uniqueResult();

        if (setting == null) {   //  Создание новой опции
            Transaction tx = getSession().beginTransaction();
            getSession().insert(new Setting(key, value, comment));
            tx.commit();
        }

        setting = (Setting) getSession().createQuery("from Setting where key=?").setString(0, key).uniqueResult();
        return setting.getValue();
    }

    /**
     * Получить числовое значение настройки или создать такую настройку, если она отсутствует. Если значение не является
     * числом, будет возвращен null.
     *
     * @param key     имя настройки
     * @param value   значение настройки
     * @param comment наименование настройки на русском
     * @return значение настройки (существующей или только что созданной)
     */
    public synchronized Integer getIntOrCreateIfNotExists(String key, int value, String comment) {
        _assertNotBlank(key);
        _assertNotBlank(comment);

        Setting setting = (Setting) getSession().createQuery("from Setting where key=?").setString(0, key).uniqueResult();
        if (setting == null) {   //  Создание новой опции
            Transaction tx = getSession().beginTransaction();
            getSession().insert(new Setting(key, String.valueOf(value), comment));
            tx.commit();
        }

        setting = (Setting) getSession().createQuery("from Setting where key=?").setString(0, key).uniqueResult();

        try {
            return Integer.parseInt(setting.getValue());
        } catch (NumberFormatException e) {
            return null;
        }
    }


    /**
     * Создать опцию в БД если таковая отсутствует. Если опция с указанным именем уже существует, ее значение НЕ будет
     * изменено
     *
     * @param key   наименование опции
     * @param value значение опции
     */
    public synchronized void createIfNotExists(String key, int value) {
        assert key != null;

        createIfNotExists(key, String.valueOf(value));
    }

    /**
     * Создать опцию в БД если таковая отсутствует. Если опция с указанным именем уже существует, ее значение НЕ будет
     * изменено
     *
     * @param key     наименование опции
     * @param value   значение опции
     * @param comment комментарий
     */
    public synchronized void createIfNotExists(String key, int value, String comment) {
        assert key != null;

        createIfNotExists(key, String.valueOf(value), comment);
    }

    //
    //  section Goods ok
    //

    /**
     * Получить перечень всех групп товаров
     */
    public List<GoodType> getGoodTypes() {
        return getGoodTypesTree(1);
    }

    /**
     * Получить корневой узел дерева групп товаров
     */
    public GoodType getRootGoodType() throws SettingNotFoundException, ValidationException, CreateFailureException {
        synchronized (this) {
            return (GoodType) get(GoodType.class, getRootGoodTypeId());
        }
    }

    /**
     * Получить список групп товаров по идентификатору родительской группы
     */
    public List<GoodType> getGoodTypesByParentId(long id) {
        synchronized (this) {
            return (List<GoodType>) getSession().createQuery("from GoodType where parentId =? and deleted = false").setLong(0, id).list();
        }
    }

    /**
     * Фиктивное удаление группы товаров по идентификатору
     *
     * @param goodTypeId
     */
    public void softRemoveGoodTypeById(long goodTypeId) throws InvalidIdException {
        if (!exists(GoodType.class, goodTypeId)) {
            throw new InvalidIdException(GoodType.class, goodTypeId);
        }

        Transaction tx = getSession().beginTransaction();
        getSession().createQuery("update GoodType set deleted=true where id=?").setLong(0, goodTypeId).executeUpdate();
        tx.commit();
    }

    /**
     * Возвращает true если группа с таким именем не существует
     *
     * @param name наименование группы
     * @param id   идентификатор группы
     * @return true если группа с таким именем не существует
     */
    public boolean isGoodTypeNameUnique(String name, long id) {
        assert name != null;
        assert id != -1;

        name = name.trim();

        GoodType goodType = (GoodType) get(GoodType.class, id);
        if (goodType == null) {
            throw new InvalidIdException(GoodType.class, id);
        }
        long parentId = goodType.getParentId();

        return (0 == (Long) getSession().createQuery("select count(*) from GoodType where parentId=? and name=? and id!=?")
                .setLong(0, parentId)
                .setString(1, name)
                .setLong(2, id)
                .uniqueResult());
    }

    /**
     * Возвращает true если группа с таким именем не существует
     *
     * @param name     наименование группы
     * @param parentId идентификатор родительской группы
     * @return true если группа с таким именем не существует
     */
    public boolean isGoodTypeNameUniqueByParent(String name, long parentId) {
        assert name != null;

        name = name.trim();

        return (0 == (Long) getSession().createQuery("select count(*) from GoodType where parentId=? and name=?")
                .setLong(0, parentId)
                .setString(1, name)
                .uniqueResult());
    }


    //
    //  section Клиенты ok
    //

    /**
     * Класс для хранения результатов процедуры определения рекомендуемой клиентской группы
     */
    public static class CalcClientGroupResult {
        /**
         * Идентификатор клиента, для которого производилась оценка
         */
        private final long clientId;

        /**
         * Идентификатор рекомендуемой клиентской группы или -1, если таковую найти не удалось
         */
        private final long clientGroupId;

        /**
         * Сумма закупок клиента за указанный отчетный период
         */
        private final float sum;

        /**
         * @param clientId      Идентификатор клиента, для которого производилась оценка
         * @param clientGroupId Идентификатор рекомендуемой клиентской группы или -1, если таковую найти не удалось
         * @param sum           Сумма закупок клиента за указанный отчетный период
         */
        public CalcClientGroupResult(long clientId, long clientGroupId, float sum) {
            this.clientId = clientId;
            this.clientGroupId = clientGroupId;
            this.sum = sum;
        }

        /**
         * Идентификатор клиента, для которого производилась оценка
         */
        public long getClientId() {
            return clientId;
        }

        /**
         * Идентификатор рекомендуемой клиентской группы или -1, если таковую найти не удалось
         */
        public long getClientGroupId() {
            return clientGroupId;
        }

        /**
         * Сумма закупок клиента за указанный отчетный период
         */
        public float getSum() {
            return sum;
        }

        @Override
        public String toString() {
            return "CalcClientGroupResult{" +
                    "clientId=" + clientId +
                    ", clientGroupId=" + clientGroupId +
                    ", sum=" + sum +
                    '}';
        }
    }


    /**
     * Процедура вычисляет рекмендуемую клиентскую группу для клиента за заданный интвервал по заданному брэнду
     *
     * @param clientId идентификатор клиента
     * @param brandId  идентификатор брэнда
     * @param begdate  дата начала интервала
     * @param enddate  дата окончания интервала
     * @see ru.niir.goodfil.db.handlers.Goodfil.CalcClientGroupResult
     */
    public CalcClientGroupResult calculateClientGroupByClientId(long clientId, long brandId, Date begdate, Date enddate) throws SettingNotFoundException {
        try {
            Client _Client = getClientById(clientId);
            Brand _Brand = getBrandById(brandId);
            SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy HH:mm");
        } catch (Exception e) {
            e.printStackTrace();
        }

        long minTime = begdate.getTime();
        long maxTime = enddate.getTime();

        float sum = 0;
        List<Order> orders = getOrdersByClientId(clientId);
        for (Order order : orders) {
            if (isBlank(order.getC1Num())) {
                continue;
            }

            long payTime = order.getCreationDate().getTime();
            if (payTime >= minTime && payTime <= maxTime) {
                //  Расчет суммы по заказу
                List<OrderItem> orderItems = getOrderItemsByOrderId(order.getId());
                for (OrderItem orderItem : orderItems) {
                    if (connGoodsBrandsCache.getBrandIdByGoodId(orderItem.getGoodId()) == brandId) {
                        sum += orderItem.getSum();
                    }
                }
            }
        }

        long clientGroupId = getClientGroupIdBySum2(brandId, sum);
        return new CalcClientGroupResult(clientId, clientGroupId, sum);
    }

    /**
     * Процедура вычисляет рекмендуемую клиентскую группу для клиента за заданный интвервал
     *
     * @param clientId идентификатор клиента
     * @param begdate  дата начала интервала
     * @param enddate  дата окончания интервала
     * @see ru.niir.goodfil.db.handlers.Goodfil.CalcClientGroupResult
     */
    public CalcClientGroupResult calculateClientGroupByClientId(long clientId, Date begdate, Date enddate) throws SettingNotFoundException {
        long minTime = begdate.getTime();
        long maxTime = enddate.getTime();

        Map<Long, Good> goodsCache = new HashMap<Long, Good>();

        float sum = 0;
        List<Order> orders = getOrdersByClientId(clientId);
        for (Order order : orders) {
            if (order.getC1PaymentDate() == null) {
                continue;
            }

            long payTime = order.getC1PaymentDate().getTime();
            if (payTime >= minTime && payTime <= maxTime) {
                addOrderInfo(order);
                sum += order.getSum();
            }
        }
        return new CalcClientGroupResult(clientId, getClientGroupIdBySum(sum), sum);
    }

    /**
     * Функция поиска клиентской группы клиента по брэнду сумме закупок. Работа функции зависит от настройки окружения
     *
     * @param brandId идентификатор брэнда
     * @param sum     сумма закупок
     * @return клиентская группа, "подходящая" для клиента
     */
    private long getClientGroupIdBySum2(long brandId, float sum) {
        boolean strategyUp = getIntOrCreateIfNotExists(".clientgroups.strategy.up", 0, "Критерий отнесения к клиентской группе") != 0;
//        boolean followNotActiveClientGroups = getIntOrCreateIfNotExists(".clientgroups.follow.not.active", 0, "Пропускать неактивные клиентские группы при определении") != 0;

        List<ClientGroup> clientGroups = getClientGroupsByBrandId(brandId);

        long resultClientGroupId = -1;
        if (strategyUp) {
            Collections.reverse(clientGroups);
            for (ClientGroup clientGroup : clientGroups) {
//                if (!clientGroup.isActive() && followNotActiveClientGroups) continue;
                if (sum >= clientGroup.getCriteriaSum()) {
                    resultClientGroupId = clientGroup.getId();
                }
            }
        } else {
            for (ClientGroup clientGroup : clientGroups) {
//                if (!clientGroup.isActive() && followNotActiveClientGroups) continue;
                if (sum <= clientGroup.getCriteriaSum()) {
                    resultClientGroupId = clientGroup.getId();
                }
            }
        }

        return resultClientGroupId;
    }

    /**
     * Функция поиска клиентской группы клиента по сумме закупок. Работа функции зависит от настройки окружения. Функция
     * вернет -1, если подходящей клиентской группы определить невозможно
     *
     * @param sum сумма закупок
     * @return клиентская группа, "подходящая" для клиента
     */
    private long getClientGroupIdBySum(float sum) {
        boolean strategyUp = getIntOrCreateIfNotExists(".clientgroups.strategy.up", 0, "Критерий отнесения к клиентской группе") != 0;

        List<ClientGroup> clientGroups = getClientGroups();

        long resultClientGroupId = -1;
        if (strategyUp) {
            Collections.reverse(clientGroups);
            for (ClientGroup clientGroup : clientGroups) {
                if (sum >= clientGroup.getCriteriaSum()) {
                    resultClientGroupId = clientGroup.getId();
                }
            }

            //  EXPERIMENTAL
            if (resultClientGroupId == -1) {
                resultClientGroupId = clientGroups.get(0).getId();
            }
        } else {
            for (ClientGroup clientGroup : clientGroups) {
                if (sum <= clientGroup.getCriteriaSum()) {
                    resultClientGroupId = clientGroup.getId();
                }
            }

            //  EXPERIMENTAL
            if (resultClientGroupId == -1) {
                resultClientGroupId = clientGroups.get(0).getId();
            }
        }

        return resultClientGroupId;
    }

    /**
     * Получить идентификатор базовой клиентской группы. Базовой является клиентская группа с минимальным значением
     * параметра "Минимальная сумма закупок за предыдущий месяц".
     *
     * @return идентификатор базовой клиентской группы
     * @see ClientGroup
     */
//    public long getMainClientGroupId()
//    {
//        List list = getSession().createQuery("select cg.id from ClientGroup cg order by active desc, criteriaSum asc").setMaxResults(1).list();
//        if (list.size() != 1) throw new ConfigurationException("Client groups list is empty, so we cannot get the basic clientgroup");
//
//        return (Long) list.get(0);
//    }
    public long getMainClientGroupId2(long brandId) {
        List list = getSession().createQuery("select cg.id from ClientGroup cg where cg.brandId = ? order by criteriaSum asc").setLong(0, brandId).setMaxResults(1).list();
        if (list.size() != 1) {   //  Для брэнда не указано ни одной колонки
            return 0;
        }

        return (Long) list.get(0);
    }

    /**
     * Получить список существующтх клиентских групп
     */
    public List<ClientGroup> getClientGroups() {
        synchronized (this) {
            return (List<ClientGroup>) getSession().createQuery("from ClientGroup where isShown = 1 order by criteriaSum").list();
        }
    }

    /**
     * Получить список клиентских групп, относящихся к брэнду
     *
     * @param brandId идентификатор брэнда
     */
    public List<ClientGroup> getClientGroupsByBrandId(long brandId) {
        synchronized (this) {
            return (List<ClientGroup>) getSession().createQuery("from ClientGroup where brandId = ? order by criteriaSum desc")
                    .setLong(0, brandId)
                    .list();
        }
    }

    private List<GoodType> getGoodTypesTree(long rootGoodTypeId) {
        List<GoodType> allGoodTypes = new ArrayList<GoodType>();

        synchronized (this) {
            allGoodTypes = getSession().createQuery("from GoodType where deleted = false").list();
        }

        List<GoodType> nodesList = allGoodTypes;

        for (GoodType gt : nodesList) {
            gt.getChildren().clear();
        }

        GoodType rootGroodType = null;
        for (int i = 0; i < nodesList.size(); i++) {
            GoodType n1 = nodesList.get(i);
            if (n1.getId() == rootGoodTypeId) {
                rootGroodType = n1;
            }

            for (int j = 0; j < nodesList.size(); j++) {
                if (i == j) {
                    continue;
                }

                GoodType n2 = nodesList.get(j);
                if (n2.getParentId() != null && n2.getParentId() == n1.getId()) {
                    n1.getChildren().add(n2);
                    n2.setParent(n1);
                }
            }
        }

        assert rootGroodType != null;
        rootGroodType.setLevel(0);
        rootGroodType.setParent(rootGroodType);

        return rootGroodType.collect();
    }

    public List<GoodType> getGoodTypesRecursivelyByParentId(long goodTypeId, boolean includeParent) {
        List<GoodType> result = getGoodTypesTree(goodTypeId);
        if (!includeParent) {
            result.remove(0);
        }
        return result;
    }

    public List<GoodType> getGoodTypePathById(long goodTypeId) {
        List<GoodType> result = new ArrayList<GoodType>();
        List<GoodType> goodTypes = getGoodTypesTree(1);
        GoodType ptr = null;
        for (GoodType goodType : goodTypes) {
            if (goodType.getId() == goodTypeId) {
                ptr = goodType;
            }
        }

        result.add(ptr);
        while (ptr.getId() != 1) {
            ptr = ptr.getParent();
            result.add(ptr);
        }
        return result;
    }

    /**
     * Получить количество клиентов с учетом фильтра
     *
     * @param filter фильтр
     * @return количество клиентов с учетом фильтра
     * @see ru.niir.goodfil.utils.filters.ClientsFilter
     */
    public long getClientsCount(ClientsFilter filter) {
        synchronized (this) {
            QueryBuilder b = QueryBuilder.WithWhere("select count(*) from Client");
            if (filter != null) {
                final Set<Long> replacedManagersIds = getReplacedManagersByAssistantId(filter.getManagerId());
//                if (filter.getClientGroupId() != -1) b.addWhereCondition("clientGroupId=?", filter.getClientGroupId());
                if (filter.getManagerId() != -1) {
                    if (filter.getManagerId() == 0) {
                        b.addWhereCondition("managerId is null");
                    } else {
                        final List<String> subqueries = new ArrayList<String>();
                        subqueries.add("managerId=" + filter.getManagerId());
                        for (Long replacedManagersId : replacedManagersIds) {
                            subqueries.add("managerId=" + replacedManagersId);
                        }

                        b.addWhereCondition("(" + StringUtils.join(subqueries, " or ") + ")");
                    }
                }
                if (!isBlank(filter.getName())) {
                    b.addWhereCondition("companyName like ?", "%" + filter.getName() + "%");
                }
            }

            return (Long) b.toQuery(getSession()).uniqueResult();
        }
    }

    /**
     * Получить список клиентов
     */
    public List<Client> getClients() {
        return getClients(null);
    }

    /**
     * Получить список клиентов с учетом фильтра
     *
     * @param filter фильтр
     * @return перечень клиентов с учетом фильтра
     * @see ru.niir.goodfil.utils.filters.ClientsFilter
     */
    public List<Client> getClients(ClientsFilter filter) {
        return getClients(0, Integer.MAX_VALUE, filter);
    }

    public List<ClientHeader> getClientHeaders() {
        synchronized (this) {
            List<ClientHeader> result = new LinkedList<ClientHeader>();
            Transaction tx = getSession().beginTransaction();
            try {
                Iterator i = getSession().createSQLQuery("select `Id`, `CompanyName` from `Clients` order by `CompanyName` asc").list().iterator();
                while (i.hasNext()) {
                    Object[] objs = (Object[]) i.next();
                    Long clientId = ((Number) objs[0]).longValue();
                    String name = (String) objs[1];
                    result.add(new ClientHeader(clientId, name));
                }
                tx.commit();
            } finally {
                if (!tx.wasCommitted()) {
                    tx.rollback();
                }
            }
            return result;
        }
    }

    /**
     * Получить список клиентов с учетом фильтра и разбиения на страницы
     *
     * @param first  индекс первого элемента на странице
     * @param size   размер страницы
     * @param filter фильтр
     * @return перечень клиентов с учетом фильтра и разбиения на страницы
     * @see ru.niir.goodfil.utils.filters.ClientsFilter
     */
    public List<Client> getClients(int first, int size, ClientsFilter filter) {
        List<Client> result;
        synchronized (this) {
            QueryBuilder b = QueryBuilder.WithWhere("from Client");
            if (filter != null) {
                final Set<Long> replacedManagersIds = getReplacedManagersByAssistantId(filter.getManagerId());

//                if (filter.getClientGroupId() != -1) b.addWhereCondition("clientGroupId=?", filter.getClientGroupId());
                if (filter.getManagerId() != -1) {
                    if (filter.getManagerId() == 0) {
                        b.addWhereCondition("managerId is null");
                    } else {
                        final List<String> subqueries = new ArrayList<String>();
                        subqueries.add("managerId=" + filter.getManagerId());
                        for (Long replacedManagersId : replacedManagersIds) {
                            subqueries.add("managerId=" + replacedManagersId);
                        }

                        b.addWhereCondition("(" + StringUtils.join(subqueries, " or ") + ")");
                    }
                }

                if(filter.getClientStateId() != -1){
                    b.addWhereCondition("clientStateID = ?", filter.getClientStateId());
                }
                if (!isBlank(filter.getName())) {
                    b.addWhereCondition("companyName like ?", "%" + filter.getName() + "%");
                }
                if (!isBlank(filter.getEmail())) {
                    b.addWhereCondition("contactEmail like ?", "%" + filter.getEmail() + "%");
                }
                if (!isBlank(filter.getAddressFact())) {
                    b.addWhereCondition("addressFact like ?", "%" + filter.getAddressFact() + "%");
                }
                if(!isBlank(filter.getRepuplic())){
                    b.addWhereCondition("republic like ?", "%"+ filter.getRepuplic() +"%");
                }
                if(!isBlank(filter.getRegion())){
                    b.addWhereCondition("region like ?", "%"+ filter.getRegion() +"%");
                }
                if(!isBlank(filter.getKray())){
                    b.addWhereCondition("kray like ?", "%"+ filter.getKray() +"%");
                }
                if(!isBlank(filter.getCity())){
                    b.addWhereCondition("city like ?", "%"+ filter.getCity() +"%");
                }
                if(!isBlank(filter.getStreet())){
                    b.addWhereCondition("street like ?", "%"+ filter.getStreet() +"%");
                }
                if(!isBlank(filter.getHouse())){
                    b.addWhereCondition("house like ?", "%"+ filter.getHouse() +"%");
                }
                if(filter.isActive()){
                    b.addWhereCondition("isActive = ?", filter.isActive());
                }
            }

            b.appendText("order by companyName");

            result = (List<Client>) b.toQuery(getSession()).setFirstResult(first).setMaxResults(size).list();
        }
        return result;
    }

    /**
     * Получить запись клиента по регистрационному номеру клиента
     *
     * @param code регистрационный номер
     * @return клиент
     */
    public Client getClientByRegistrationCode(String code) {
        code = s(code);
        if (code.equals("")) {
            return null;
        }

        return (Client) getSession().createQuery("from Client where registrationCode=?").setString(0, code).uniqueResult();
    }

    /**
     * Возвращает true, если в БД существует РОВНО ОДИН клиент с указанным регистрационным номером
     *
     * @param code регистрационный номер
     * @return true, если в БД существует РОВНО ОДИН клиент с указанным регистрационным номером
     */
    public boolean isClientByRegistrationCode(String code) {
        code = s(code);
        if (code.equals("")) {
            return false;
        }

        return (1 == (Long) getSession().createQuery("select count(*) from Client where registrationCode=?").setString(0, code).uniqueResult());
    }

    /**
     * Возвращает количество клиентов в данной клиентской группе
     *
     * @param id идентификатор клиентской группы
     * @return количество клиентов
     */
//    public long getClientsCountByClientGroupId(long id)
//    {
//        synchronized (this)
//        {
//            return (Long) getSession().createQuery("select count(*) from Client where clientGroupId =?").setLong(0, id).uniqueResult();
//        }
//    }

    /**
     * Получить список клиентов, относящихся с заданной клиентской группе
     *
     * @param id идентификатор клиентской группы
     */
//    public List<Client> getClientsByClientGroupId(long id)
//    {
//        synchronized (this)
//        {
//            return (List<Client>) getSession().createQuery("from Client where clientGroupId =?").setLong(0, id).list();
//        }
//    }

    /**
     * Получить список клиентов, связанных с данным менеджером
     *
     * @param id идентификатор менеджера
     */
    public List<Client> getClientsByManagerId(long id) {
        Query q = getSession().createQuery("from Client where managerId =?");
        q.setLong(0, id);
        System.out.println("> " + q.getQueryString());
        return (List<Client>) q.list();
    }

    /**
     * Получить пользователя, связанного с данным клиентом
     *
     * @param clientId идентификатор клиента
     * @return пользователь, связанный с данным клиентом или null, если таковой отсутствует
     */
    public User getUserByClientId(long clientId) {
        synchronized (this) {
            return (User) getSession().createQuery("from User where clientId=?").setLong(0, clientId).uniqueResult();
        }
    }

    /**
     * Получить число заказов клиента
     *
     * @return число заказов
     */
    public long getOrdersCountByClientId(long clientId) {
        synchronized (this) {
            return (Long) getSession().createQuery("select count(*) from Order where clientId=? and isDeleted=?").setLong(0, clientId).setBoolean(1, false).uniqueResult();
        }
    }

    /**
     * Активизация клиента (используется для подтверждения адреса эл.почты)
     *
     * @param clientId идентификатор клиента
     */
    public void activateClient(long clientId) throws InvalidIdException {
        if (!exists(Client.class, clientId)) {
            throw new InvalidIdException(Client.class, clientId);
        }

        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createQuery("update Client set active=1 where id = ?").setLong(0, clientId).executeUpdate();
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    /**
     * Удалить позицию из заказа, если количество товаров в ней равно нулю и обновить информацию о позиции заказа в
     * противоположном случае.
     *
     * @param orderItem позиция заказа
     * @throws RemoveFailureException ошибка удаления позиции заказа
     * @throws ValidationException    позиция заказа содержит ошибки
     * @throws InvalidIdException     позиция заказа отсутствует в БД
     */
    public void cleverUpdateOrderItem(OrderItem orderItem) throws RemoveFailureException, ValidationException, InvalidIdException {
        if (orderItem.getCount() == 0) {
            removeOrderItem(orderItem.getId());
        } else {
            updateOrderItem(orderItem);
        }
    }

    //  section Пользователи ok

    /**
     * Сгенерировать SELECT запрос к базе для выборки пользователей (таблица Users).
     *
     * @param filter    фильтр отбора пользователей
     * @param countOnly true, если требуется вернуть не коллекцию пользователей, а только их колдичество
     * @return запрос (hibernate query)
     */
    private Query generateUsersQuery(UsersFilter filter, boolean countOnly) {
//        logger.debug("getUsers " + (countOnly ? "(count): " : "(query): ") + filter);
        QueryBuilder b = countOnly ? QueryBuilder.WithWhere("select count(*) from User u") : QueryBuilder.WithWhere("from User u");

        if (filter != null) {
            if (!isBlank(filter.getFio())) {
                b.addWhereCondition("u.fio like ?", "%" + filter.getFio() + "%");
            }
            if (!isBlank(filter.getLogin())) {
                b.addWhereCondition("u.login like ?", "%" + filter.getLogin() + "%");
            }
            if (!isBlank(filter.getEmail())) {
                b.addWhereCondition("u.email like ?", "%" + filter.getEmail() + "%");
            }
            if (filter.getRoleId() != -1) {
                b.addWhereCondition("u.userRoleId =?", filter.getRoleId());
            }
            if (filter.getParentManagerId() != -1) {
                b.addWhereCondition("u.parentManagerId =?", filter.getParentManagerId());
            }
            if (filter.getId() != -1) {
                b.addWhereCondition("u.id =?", filter.getId());
            }
            if (filter.getClientId() != -1) {
                b.addWhereCondition("u.clientId =?", filter.getClientId());
            }
        }

        return b.toQuery(getSession());
    }

    /**
     * Получить список пользователей с учетом фильтра
     */
    public List<User> getUsers(UsersFilter filter) {
        synchronized (this) {
            return (List<User>) generateUsersQuery(filter, false).list();
        }
    }

    /**
     * Получить количество пользователей с учетом фильтра
     */
    public long getUsersCount(UsersFilter filter) {
        synchronized (this) {
            return (Long) generateUsersQuery(filter, true).uniqueResult();
        }
    }

    /**
     * Получить список пользователей с учетом фильтра и разбиением на страницы
     */
    public List<User> getUsers(int first, int size, UsersFilter filter) {
        synchronized (this) {
            return (List<User>) generateUsersQuery(filter, false).setFirstResult(first).setMaxResults(size).list();
        }
    }

    /**
     * Получить перечень пользователей
     */
    public List<User> getUsers() {
        synchronized (this) {
            return (List<User>) get(User.class);
        }
    }

    /**
     * Получить список пользователей, относящихся с данной роли
     */
    public List<User> getUsersByRoleId(int id) {
        synchronized (this) {
            return (List<User>) getSession().createQuery("from User where userRoleId =? and isActive = 1").setInteger(0, id).list();
        }
    }

    /**
     * Проверить, есть ли пользователь с указанным табельным номером
     *
     * @param num табельный номер
     * @return true, если такой пользователь есть
     */
    public boolean isUserByNum(String num) {
        assert num != null;
        num = num.trim();

        synchronized (this) {
            return getSession().createQuery("from User where num =?").setString(0, num).list().size() > 0;
        }
    }

    /**
     * Проверить, есть ли пользователь с указанным табельным номером, кроме данного.
     * <p/>
     * Формулировка "кроме данного" обозначает следующее: Эта функция используется при редактировании данных
     * пользователя. Когда оператор меняет табельный номер, ему важно знать, что такого табельного номера в системе нет.
     * При этой проверке необходимо игнорировать того пользователя, редактирование которого производится. Ведь если это
     * не сделать, то оператор не сможет оставить тот же табельный номер, который был (потому что такой табельный номер
     * в системе, безусловно, есть - это табельный номер редактируемого пользователя).
     * <p/>
     * Если вы не поняли, то я не виноват - старался написать максимально "не криво"
     *
     * @param id  идентификатор данного пользователя
     * @param num табельный номер
     * @return true, если такой пользователь есть
     */
    public boolean isUserNumUnique(long id, String num) {
        synchronized (this) {
            return (0 == (Long) getSession().createQuery("select count (*) from User where num =? and id !=?").setString(0, num).setLong(1, id).uniqueResult());
        }
    }

    /**
     * Отправить сообщение по почте
     *
     * @param address адрес получателя
     * @param text    текст сообщения
     */
    public void sendEmailMsg(String address, String text) throws MailerException {
        Assert.notBlank(address);
        Assert.notBlank(text);

        String host = get(".smtp.host");
        String port = get(".smtp.port");
        String username = get(".smtp.username");
        String password = get(".smtp.password");

        Assert.notBlank(host);
        Assert.notBlank(port);
        Assert.notBlank(username);
        Assert.notBlank(password);

        try {
            sendEmail(host, port, username, password, address, "Goodwill", text);
        } catch (MessagingException e) {
            throw new MailerException(e);
        }
    }

    /**
     * Отправить сообщение по почте
     *
     * @param address адрес получателя
     * @param text    текст сообщения
     */
    public void sendEmailMsg(String address, String subject, String text) throws MailerException {
        Assert.notBlank(address);
        Assert.notBlank(text);
        Assert.notBlank(subject);

        String host = get(".smtp.host");
        String port = get(".smtp.port");
        String username = get(".smtp.username");
        String password = get(".smtp.password");

        Assert.notBlank(host);
        Assert.notBlank(port);
        Assert.notBlank(username);
        Assert.notBlank(password);

        try {
            sendEmail(host, port, username, password, address, subject, text);
        } catch (MessagingException e) {
            throw new MailerException(e);
        }
    }

    private static void sendEmailWithAttachment(String smtpHost, String port, String username, String password, String email, String caption, String body, String attachmentFile) throws MessagingException {
        Properties props = new Properties();
        props.put("mail.smtp.host", smtpHost);
        props.put("mail.smtp.port", new Integer(port));
        props.put("mail.smtp.auth", "true");

        Session session = Session.getInstance(props, new SMTPAuthenticator(username, password));

        String[] emails = email.split(";");
        for (String em : emails) {
            em = em.trim();
            if (em.equals("")) {
                continue;
            }

            MimeMessage message = new MimeMessage(session);
            message.setFrom(new InternetAddress(username));
            message.setSubject(caption, "UTF-8");

            Multipart multipart = new MimeMultipart();

            MimeBodyPart messageBodyPart = new MimeBodyPart();
            messageBodyPart.setText(body, "UTF-8");
            multipart.addBodyPart(messageBodyPart);

            messageBodyPart = new MimeBodyPart();

            DataSource source = new FileDataSource(attachmentFile);
            messageBodyPart.setDataHandler(new DataHandler(source));
            messageBodyPart.setFileName("Ostatki.xls");
            multipart.addBodyPart(messageBodyPart);
            message.setContent(multipart);
            message.addRecipient(Message.RecipientType.TO, new InternetAddress(em));

            Transport.send(message);
        }
    }

    private static void sendEmail(String smtpHost, String port, String username, String password, String email, String caption, String body) throws MessagingException {
        Properties props = new Properties();
        props.put("mail.smtp.host", smtpHost);
        props.put("mail.smtp.port", new Integer(port));
        //props.put("mail.smtp.auth", "true");

        Session session = Session.getInstance(props, new SMTPAuthenticator(username, password));

        String[] emails = email.split(";");
        for (String em : emails) {
            em = em.trim();
            if (em.equals("")) {
                continue;
            }

            MimeMessage message = new MimeMessage(session);
            message.setFrom(new InternetAddress(username));
            message.setSubject(caption, "UTF-8");

            Multipart multipart = new MimeMultipart();

            MimeBodyPart messageBodyPart = new MimeBodyPart();
            messageBodyPart.setText(body, "UTF-8");
            multipart.addBodyPart(messageBodyPart);

            message.setContent(multipart);
            message.addRecipient(Message.RecipientType.TO, new InternetAddress(em));

            Transport.send(message);
        }
    }

    /**
     * Сгенерировать пароль
     */
    public static String generatePassword() {
        char[] alphabet = "qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPLKJHGFDSAZXCVBNM1234567890".toCharArray();
        return new RandPass(alphabet).getPass(4);
    }

    /**
     * Генерация пароля для пользователя и его отправка на эл.адрес пользователя.
     *
     * @param userId      идентификатор пользователяю
     * @param sendByEmail флаг, если он равен true, пароль будет выслан на эл.адрес пользователя, если false - то никаких уведомлений не будет, пароль просто будет изменен.
     * @throws ValidationException
     * @throws MailerException     возникает при ошибке отправки письма на эл.адрес.
     * @see ValidationException
     */
    public void sendPasswordForUser(Long userId, boolean sendByEmail) throws ValidationException, MailerException {
        Assert.notNull(userId);

        User user = getUserById(userId);
        Assert.notNull(user);

        String password = generatePassword();
        user.setPassword(password);
        updateUser(user);

        if (sendByEmail) {
            String email = null;
            String fio = null;

            if (user.getIsClient()) {
                Client client = getClientById(user.getClientId());
                email = client.getContactEmail();
                fio = client.getContactFio();
            } else {
                email = user.getEmail();
                fio = user.getFio();
            }

            Assert.notNull(fio);
            Assert.notNull(email);

            String subject = "Имя пользователя для доступа к ИНТЕРНЕТ-системе";

            StringBuilder sb = new StringBuilder();
            sb.append("Уважаемый(-ая) " + fio + "!\n");
            sb.append("Ниже представлены данные, необходимые Вам для использования ИНТЕРНЕТ-системы: \n");
            sb.append("Ваше имя пользователя: " + user.getLogin() + "\n");
            sb.append("Ваш пароль: " + password + "\n");
            sb.append(ISSignature.legal());

            sendEmailMsg(email, subject, sb.toString());

            getAuditService().logPasswordChange(getAuditHelper().getIp(),
                    getAuditHelper().getLogin(),
                    user.getLogin(),
                    email);
        } else {
            getAuditService().logPasswordChange(getAuditHelper().getIp(),
                    getAuditHelper().getLogin(),
                    user.getLogin(),
                    "Пароль на email не отправлялся");
        }
    }

    /**
     * Получить перечень пользователей с ролью "менеджер"
     */
    public List<User> getManagers() {
        synchronized (this) {
            return (List<User>) getSession().createQuery("from User where userRoleId =? and isActive = 1 order by fio").setInteger(0, Role.MANAGER).list();
        }
    }

    /**
     * Проверить, если ли в БД пользователь с указанным логином, кроме данного пользователя.
     * <p/>
     * Формулировка "кроме данного" обозначает следующее: Эта функция используется при редактировании данных
     * пользователя. Когда оператор меняет логин, ему важно знать, что такого логина в системе нет. При этой проверке
     * необходимо игнорировать того пользователя, редактирование которого производится. Ведь если это не сделать, то
     * оператор не сможет оставить тот же логин, который был (потому что такой логин в системе, безусловно, есть - это
     * логин редактируемого пользователя).
     * <p/>
     * Если вы не поняли, то я не виноват - старался написать максимально "не криво"
     *
     * @param id    идентификатор данного пользователя
     * @param login логин
     * @return true, если логин уникален
     */
    public boolean isUserLoginUnique(String login, long id) {
        assert login != null;
        login = login.trim();

        synchronized (this) {
            return (0 == (Long) getSession().createQuery("from User where id !=? and login =?").setLong(0, id).setString(1, login).uniqueResult());
        }
    }

    /**
     * Авторизация пользователя
     *
     * @param login    логин
     * @param password пароль
     * @return true, если пользователь проходит аутентификацию
     */
    public boolean login(String login, String password) {
        Assert.notBlank(login);
        Assert.notBlank(password);

        login = login.trim();
        password = password.trim();

        int authorizationSystem = getIntOrCreateIfNotExists(".authorization.system", 1, "Тип системы авторизации (1 или 2)");

        if (authorizationSystem == 1) {
            //
            //  Простой механизм авторизации - просто проверяется совпадение логина/пароля.
            //
            User user = (User) getSession().createQuery("from User where login=? and password=?").setString(0, login).setString(1, password).uniqueResult();
            if (user == null) {
                return false;
            }

            if (!user.getIsClient()) {
                return true;
            } else {
                Client client = getClientById(user.getClientId());
                if (client == null) {
                    return false;
                }
                if (!client.isActive()) {
                    return false;
                }

                return true;
            }
        } else {
            //
            //  Усложненный алгоритм авторизации, позволяющий заходить менеджером за клиентов под своим паролем.
            //
            User user = (User) getSession().createQuery("from User where login = ?").setString(0, login).uniqueResult();
            if (user == null) {
                return false;
            }

            if (user.getIsClient()) {
                Long clientId = user.getClientId();
                Client client = getClientById(clientId);
                if (client == null || !client.isActive()) {
                    return false;
                }
                Long managerId = client.getManagerId();
                User manager = null;
                if (managerId != null) {
                    manager = getUserById(managerId);
                }

                boolean clientPasswordEquals = user.getPassword().equals(password);
                boolean managerPasswordEquals = false;
                if (manager != null) {
                    managerPasswordEquals = manager.getPassword().equals(password);
                }

                return clientPasswordEquals || managerPasswordEquals;
            } else {
                return user.getPassword().equals(password);
            }
        }
    }

    /**
     * Получить пользователя по его логину.
     *
     * @param login логин пользователя
     */
    public User getUserByLogin(String login) {
        Assert.notBlank(login);

        return (User) getSession().createQuery("from User where login=?").setString(0, login).uniqueResult();
    }

    /**
     * Получить пользователя по его табельному номеру.
     *
     * @param tabNumber табельный номер
     */
    public User getUserByTab(String tabNumber) {
        Assert.notBlank(tabNumber);

        return (User) getSession().createQuery("from User where num=?").setString(0, tabNumber).uniqueResult();
    }

    //
    //  section Заказы ok
    //

    /*
            === УДАЛЕНИЕ ЗАКАЗОВ ===

            Для удаления заказа следует вызывать не функцию removeOrder, а функцию softRemoveOrder.
            Это нужно для того, чтобы подсистема интеграции с 1С могла корректно обработать удаление заказа.

            Схема такая:
            1. Заказ помечается "на удаление" (флаг isChanged=true)
            2. Подсистема интеграции с 1С при очередной выгрузке отправляет в 1С информацию о "фиктивно удаленных" заказах
            3. После чего подсистема интеграции с 1С и удаляет эти заказы
     */

    /**
     * Функция фикативного удаления заказа
     *
     * @param id идентификатор заказа
     */
    public void softRemoveOrder(long id) throws IllegalArgumentException, ValidationException {
        Order order = getOrderById(id);

        /*
        Если заказ уже "как бы удален", то как бы удалять его второй раз не нужно
         */
        if (order.isDeleted()) {
            return;
        }

        if(getIsReservedOn() && !order.getOrderStateIsPreSaved()){
            updateGoodCountByDeletingOrder(order.getId(), order.getOrderStateId());
        }

        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createQuery("update Order set isDeleted=? where id=?").setBoolean(0, true).setLong(1, id).executeUpdate();

            /*
            В статусах presaved, saved, awaiting validation и not validated - data exchange добавлять не нужно
             */
            if(!getIsReservedOn()){
                if (!order.getOrderStateIsPreSaved() &&
                        !order.getOrderStateIsSaved() &&
                        !order.getOrderStateIsAwaitingValidation() &&
                        !order.getOrderStateIsNotValidated()) {
                    logger.debug("Adding data exchange while soft order remove");
                    DataExchange dataExchange = new DataExchange();
                    dataExchange.setTableName("Orders");
                    dataExchange.setItemId(id);
                    getSession().insert(dataExchange);
                }
            }else {
                if(order.getOrderStateIsOnDelivery() || order.getOrderStateIsComplete()){
                    DataExchange dataExchange = new DataExchange();
                    dataExchange.setTableName("Orders");
                    dataExchange.setItemId(id);
                    getSession().insert(dataExchange);
                }
            }

            tx.commit();
        } catch (Exception e) {
            logger.error(e);
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    public List<Order> getRemovedOrders() {
        synchronized (this) {
            return getSession().createQuery("from Order where isDeleted=?").setBoolean(0, true).list();
        }
    }

    /**
     * Функция удаляет из БД все пястые заказы, созданные более месяца назад. Не удалось выполнить эту операцию одним
     * запросом, поэтому сначала выбираются все такие заказы, а потом удаляются по одному.
     *
     * @return количество удаленных записей БД
     */
    public int removeEmptyOrders() {
        /**
         * Запрос для удаления из БД всех пустых заказов, созданных более месяца назад.
         */
        final String QUERY = "" +
                "select o.Id from `Orders` o " +
                "where " +
                "((select count(*) from `OrderItems` oi where oi.IdOrder = o.Id) = 0) and " +
                "(MONTH(NOW()) - MONTH(o.CreationDate) >= 1)";

        int result = 0;
        Transaction tx = getSession().beginTransaction();
        try {
            List l = getSession().createSQLQuery(QUERY).list();
            Iterator it = l.iterator();
            while (it.hasNext()) {
                long id = ((BigInteger) it.next()).longValue();
                result += getSession().createQuery("delete from Order where id=?").setLong(0, id).executeUpdate();
            }
            tx.commit();
        } catch (Exception e) {
            logger.error(e);
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
        return result;
    }

    /**
     * Установить статус заказа
     * <p/>
     * Проверки: 1. Пустые заказы не допускаются. 2. Переход заказа по статусам допускается только в соответствии с
     * таблицей переходов
     *
     * @param orderId
     * @param orderState
     */
    public void setOrderState(long orderId, int orderState) throws EmptyOrderException, InvalidIdException, OrderStateChangeException {
        createIfNotExists("system.control.states", 1);
        int systemControlStates = getInt("system.control.states");

        Order order = getOrderById(orderId);
        if (order == null) {
            throw new InvalidIdException(Order.class, orderId);
        }

        // Если резервы выключены, то запрещаем дальнейшее редактирование заказа.
/*        if(!getIsReservedOn()){
            order.setEditable(false);
        }*/

        int oldOrderState = order.getOrderStateId();

        if (oldOrderState == orderState) {
            return;
        }

        List<OrderItem> orderItems = getOrderItemsByOrderId(orderId);

        Map<Integer, Integer[]> map = new HashMap<Integer, Integer[]>();
        map.put(OrderState.PRE_SAVED, new Integer[]{OrderState.AWAITING_GOODS, OrderState.SAVED, OrderState.AWAITING_VALIDATION});
        map.put(OrderState.SAVED, new Integer[]{OrderState.AWAITING_VALIDATION});
        map.put(OrderState.AWAITING_VALIDATION, new Integer[]{OrderState.VALIDATED, OrderState.NOT_VALIDATED, OrderState.OUTPUT, OrderState.DELIVERY});
        map.put(OrderState.NOT_VALIDATED, new Integer[]{OrderState.VALIDATED});
        map.put(OrderState.VALIDATED, new Integer[]{OrderState.ACCEPTED_BY_BUH});
        map.put(OrderState.ACCEPTED_BY_BUH, new Integer[]{OrderState.PAYED});
        map.put(OrderState.PAYED, new Integer[]{OrderState.OUTPUT});
        map.put(OrderState.OUTPUT, new Integer[]{OrderState.DELIVERY});
        map.put(OrderState.DELIVERY, new Integer[]{OrderState.BACK, OrderState.COMPLETE});

        if (orderItems.size() == 0 && order.getDeliveryTypeId() != DeliveryType.TRANSPORT_COMPANY_RETURN) {
            throw new EmptyOrderException(orderId);
        }

        boolean ok = false;
        for (int i : map.get(oldOrderState)) {
            if (orderState == i) {
                ok = true;
            }
        }

        if ((ok && systemControlStates != 0) || systemControlStates == 0) {
            updateOrderState(orderId, orderState);

            if (orderState == OrderState.VALIDATED) {
                DataExchange de = new DataExchange();
                de.setItemId(orderId);
                de.setTableName("Orders");

                //  UNSAFE: Нужно ли здесь обрабатывать runtime исключения?
                Transaction tx = getSession().beginTransaction();
                getSession().insert(de);
                tx.commit();
            }
        } else {
            throw new OrderStateChangeException(orderId, oldOrderState, orderState);
        }
    }

    /**
     * Установить статус заказа
     *
     * @param orderId    идентификатор заказа
     * @param orderState статус заказа
     */
    public void setOrderState_Unsafe(long orderId, int orderState) {
        updateOrderState(orderId, orderState);
    }

    private void updateOrderState(long orderId, int orderState) {
        /*
        Добавляем информацию в лог
         */
        Logger logger = Logger.getLogger("OrderStatusChange");
        Order oldOrder = getOrderById(orderId);
        if (orderState != oldOrder.getOrderStateId()) {
            Client client = getClientById(oldOrder.getClientId());
            User manager = getUserById(oldOrder.getManagerId());

            StringBuilder sb = new StringBuilder();
            sb.append("ORDER " + orderId + " STATUS CHANGED FROM " +
                    oldOrder.getOrderStateId() + " [" + Transliter.translit(oldOrder.getOrderStateAsString()) + "] " +
                    "TO " + orderState + " [" + Transliter.translit(OrderState.asString(orderState)) + "]");
            sb.append(" (client: " + client.getId() + " " + Transliter.translit(client.getCompanyName()) + ",");
            sb.append(" manager: " + manager.getId() + " " + Transliter.translit(manager.getFio()) + ",");
            sb.append(" user: " + Transliter.translit(getUsername()) + ",");
            sb.append(" from: Goodfil.updateOrderState)");
            logger.debug(sb.toString());
        }

        //  UNSAFE: Нужно ли здесь обрабатывать runtime исключения?
        Transaction tx = getSession().beginTransaction();
        getSession().createQuery("update Order set orderStateId=? where id=?").setInteger(0, orderState).setLong(1, orderId).executeUpdate();
        tx.commit();
    }

    /**
     * Сгенерировать SELECT запрос для выборки заказов
     *
     * @param filter    фильтр
     * @param countOnly true, если требуется выбрать не зказаы, а только их количество
     * @return запрос (hibernate query)
     */
    private Query generateOrdersQuery(OrdersFilter filter, boolean countOnly) {
        QueryBuilder b = countOnly ? QueryBuilder.WithWhere("select count(*) from Order") : QueryBuilder.WithWhere("from Order");
        b.addWhereCondition("isDeleted=?", false);

        if (filter != null) {
            final Set<Long> replacedManagersIds = getReplacedManagersByAssistantId(filter.getManagerId());


            if (filter.getOrderId() != null && filter.getOrderId() > 0) {
                System.out.println("> " + filter.getOrderId());
                b.addWhereCondition("id=?", filter.getOrderId());
            }
            if (filter.getClientId() != -1) {
                b.addWhereCondition("clientId=?", filter.getClientId());
            }
            if (filter.getManagerId() != -1) {
                final List<String> subqueries = new ArrayList<String>();
                subqueries.add("managerId=" + filter.getManagerId());
                for (Long replacedManagersId : replacedManagersIds) {
                    subqueries.add("managerId=" + replacedManagersId);
                }

                b.addWhereCondition("(" + StringUtils.join(subqueries, " or ") + ")");
            }
            if (filter.getDeliveryTypeId() != -1) {
                b.addWhereCondition("devliveryTypeId=?", filter.getDeliveryTypeId());
            }
            if (filter.getOrderStateId() != -1) {
                b.addWhereCondition("orderStateId=?", filter.getOrderStateId());
            }
            if (!StringUtils.isBlank(filter.getC1Num())) {
                b.addWhereCondition("c1Num=?", filter.getC1Num());
            }
            if (!StringUtils.isBlank(filter.getNoteNum())) {
                b.addWhereCondition("noteNum=?", filter.getNoteNum());
            }
            if (filter.getBegDate() != null) {
                b.addWhereCondition("creationDate>=?", filter.getBegDate());
            }
            if (filter.getEndDate() != null) {
                b.addWhereCondition("creationDate<=?", filter.getEndDate());
            }
            if (filter.getDelBeginDate() != null) {
                b.addWhereCondition("deliveryDate>=?", filter.getDelBeginDate());
            }
            if (filter.getDelEndDate() != null) {
                b.addWhereCondition("deliveryDate<=?", filter.getDelEndDate());
            }
        }

        System.out.println("> " + b.getQueryString());
        return b.toQuery(getSession());
    }

    /**
     * Получить количество заказов, удовлетоваряющих указанному фильтру
     *
     * @param filter фильтр
     * @return количество заказов
     */
    public long getOrdersCount(OrdersFilter filter) {
        synchronized (this) {
            return (Long) generateOrdersQuery(filter, true).uniqueResult();
        }
    }

    private Query generateStorageReturnsQuery(StorageReturnsFilter filter) throws GoodfilException {
        if(filter == null) throw new GoodfilException("Произошла ошибка. Пожалуйста, попробуйте позже или свяжитесь с администратором");
        QueryBuilder queryBuilder = QueryBuilder.WithWhere("from StorageReturn");

        queryBuilder.addWhereCondition("isConfirm=0");
        if (filter.getSrorageReturnId() != -1){
            queryBuilder.addWhereCondition("id=?", filter.getSrorageReturnId());
        }
        if (filter.getClientId() != -1){
            queryBuilder.addWhereCondition("clientId=?", filter.getClientId());
        }
        if (!isBlank(filter.getGoodCode())){
            Good good = getGoodByCode(filter.getGoodCode().toUpperCase());
            if(good != null){
                queryBuilder.addWhereCondition("goodId=?", good.getId());
            }
        }
        if (filter.getBeginDate() != null){
            queryBuilder.addWhereCondition("creationDate >=?", filter.getBeginDate());
        }
        if (filter.getEndDate() != null){
            queryBuilder.addWhereCondition("creationDate <=?", filter.getEndDate());
        }
        return queryBuilder.toQuery(getSession());
    }

    public List<StorageReturn> getStorageReturns(StorageReturnsFilter filter) throws GoodfilException {
        return (List<StorageReturn>)generateStorageReturnsQuery(filter).list();
    }

    /**
     * Дле переданного заказа посчитать количество позиций и стоимость заказа без доставки
     *
     * @param order заказ
     */
    public void countOrderSum(Order order) {
        assert order != null;

        List<OrderItem> items = getOrderItemsByOrderId(order.getId());
        order.setPositionsCount(items.size());

        float sum = 0;
        for (OrderItem orderItem : items) {
            sum += orderItem.getSum();
        }
        order.setSum(sum);
    }

    public void packageCountMultiplies(Order order) {
        assert order != null;
        boolean flag = true;
        List<OrderItem> items = getOrderItemsByOrderId(order.getId());
        if (items.size() == 0) {
            flag = false;
        } else {
            for (OrderItem orderItem : items) {
                orderItem.setGood(getGoodById(orderItem.getGoodId()));
                if (orderItem.getCount() % Integer.parseInt(orderItem.getGood().getPackageCount()) != 0) {
                    flag = false;
                    break;
                }
            }
        }

        order.setPackageCountMultiplies(flag);

    }

    public void countOrderFactSum(Order order) {
        assert order != null;

        List<OrderItem> items = getOrderItemsByOrderId(order.getId());
        order.setPositionsCount(items.size());

        float sum = 0;
        for (OrderItem orderItem : items) {
            sum += orderItem.getOutOrderSum();
        }
        order.setFactSum(sum);
    }

    /**
     * Для переданного заказа посчитать стоимость доставки НА ОСНОВЕ СТОИМОСТИ БЕЗ ДОСТАВКИ (т.е. поле Order.sum уже
     * должно быть корректно заполнено)
     *
     * @param order заказ
     */
    public void countOrderDeliverySum(Order order) throws SettingNotFoundException {
        assert order != null;

        int deliveryCompanyMoney = getInt("delivery.company.money");
        int deliveryFreeMoney = getInt("delivery.free.money");
        int deliveryEdge = getInt("delivery.edge");

        float sum = order.getSum();

        float deliverySum = order.getDeliveryTypeId() == DeliveryType.COMPANY ? (sum >= deliveryEdge ? deliveryFreeMoney : deliveryCompanyMoney) : 0;
        order.setDeliverySum(deliverySum);
    }

    /**
     * Для переданного заказа посчитать количество позиций, стоимость без доставки и стоимость доставки
     *
     * @param order заказ
     */
    public void addOrderInfo(Order order) throws SettingNotFoundException {
        assert order != null;

        countOrderSum(order);
        countOrderDeliverySum(order);
        countOrderFactSum(order);
        packageCountMultiplies(order);
    }

    /**
     * Возвращает перечень заказов на сборку. Это: 1. Неудаленные заказы 2. Статус "сборка заказа" 3. У заказа есть
     * номер накладной 1С 4. Дата отгрузки попадает в интервал (задается в системных настройках)
     */
    public List<Order> getOrdersForBuild(int days) {
        Calendar now = Calendar.getInstance();
        now.set(Calendar.HOUR_OF_DAY, 0);
        now.set(Calendar.MINUTE, 0);
        now.set(Calendar.SECOND, 0);
        now.set(Calendar.MILLISECOND, 0);
        now.add(Calendar.DATE, days);


        Date enddate = new Date(now.getTimeInMillis());

/*        return (List<Order>) getSession().createQuery("from Order o where (o.deliveryDate is null or (o.deliveryDate != null and o.deliveryDate <= ?)) and o.isDeleted = false and o.orderStateId = ? and o.c1Num != null")
                .setDate(0, enddate)
                .setInteger(1, OrderState.OUTPUT)
                .list();*/
        return (List<Order>) getSession().createQuery("from Order o where (o.deliveryDate != null and o.deliveryDate <= ?) and o.isDeleted = false and (o.orderStateId = ? or o.orderStateId = ?) order by o.creationDate, o.clientId")
                .setDate(0, enddate)
                .setInteger(1, OrderState.OUTPUT)
                .setInteger(2, OrderState.ACCEPTED_BY_STORAGE)
                .list();
    }

    /**
     * Возвращает перечень заказов на выдачу, которые отгружаются в срок. Это: 1. Неудаленные заказы. 2. Статус "выдача заказа" 3. У заказа есть
     * номер накладной 1С 4. Дата отгрузки попадает в интервал (задается в системных настройках)
     * 5. Дата отгрузки больше текущей даты
     */
    public List<Order> getOrdersForOut(int days) {
        Calendar now = Calendar.getInstance();
        now.set(Calendar.HOUR_OF_DAY, 0);
        now.set(Calendar.MINUTE, 0);
        now.set(Calendar.SECOND, 0);
        now.set(Calendar.MILLISECOND, 0);
        now.add(Calendar.DATE, days);
/*        if (now.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY) {
            now.add(Calendar.DATE, 4);
        } else if (now.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
            now.add(Calendar.DATE, 3);
        } else if (now.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) {
            now.add(Calendar.DATE, 2);
        } else {
            now.add(Calendar.DATE, 2);
        }*/

/*        Calendar now1 = Calendar.getInstance();
        now1.set(Calendar.HOUR_OF_DAY, 0);
        now1.set(Calendar.MINUTE, 0);
        now1.set(Calendar.SECOND, 0);
        now1.set(Calendar.MILLISECOND, 0);

        Date currenDate = new Date(now1.getTimeInMillis());*/
        Date enddate = new Date(now.getTimeInMillis());

/*        return (List<Order>) getSession().createQuery("from Order o where o.isDeleted = false and o.orderStateId = ? and o.deliveryDate between ? and ? and o.c1Num != null")
                .setInteger(0, OrderState.DELIVERY)
                .setDate(1, currenDate)
                .setDate(2, enddate)
                .list();*/

        return (List<Order>) getSession().createQuery("from Order o where o.isDeleted = false and o.orderStateId = ? and o.deliveryDate <= ? and o.c1Num != null order by assemblyDate")
                .setInteger(0, OrderState.DELIVERY)
                .setDate(1, enddate)
                .list();
    }

    /**
     * Список просроченных по дате отгрузки заказов на выдачу. Тоже самое, что и обычные заказы на выдачу, но дата отгрузки меньше сегодняшней
     *
     * @return
     */
    public List<Order> getDelayOrdersForOut() {
        Calendar now = Calendar.getInstance();
        now.set(Calendar.HOUR_OF_DAY, 0);
        now.set(Calendar.MINUTE, 0);
        now.set(Calendar.SECOND, 0);
        now.set(Calendar.MILLISECOND, 0);
       /* if (now.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY) {
            now.add(Calendar.DATE, 4);
        } else if (now.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
            now.add(Calendar.DATE, 3);
        } else if (now.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) {
            now.add(Calendar.DATE, 2);
        } else {
            now.add(Calendar.DATE, 2);
        }*/

        Date enddate = new Date(now.getTimeInMillis());

        return (List<Order>) getSession().createQuery("from Order o where o.isDeleted = false and o.orderStateId = ? and o.deliveryDate < ? and o.c1Num != null")
                .setInteger(0, OrderState.DELIVERY).setParameter(1, enddate)
                .list();

/*        return (List<Order>) getSession().createQuery("from Order o where o.isDeleted = false and o.orderStateId = ? and o.deliveryDate <= ? and o.c1Num != null")
                .setInteger(0, OrderState.DELIVERY)
                .setDate(1, enddate)
                .list();*/
    }

    public List<Order> getReservedOrder() {
        return (List<Order>) getSession().createQuery("from Order o where o.deliveryDate is null and o.isDeleted = false and o.orderStateId = ? and o.c1Num != null").setParameter(0, OrderState.OUTPUT).list();
    }

    /**
     * Получить перечень заказов с учетом фильтра и разбиения на страницы
     *
     * @param first  иднекс первого возвращаемого элемента
     * @param size   размер страницы (количество записей)
     * @param filter фильтр
     * @return перечень заказов
     * @see ru.niir.goodfil.utils.filters.OrdersFilter
     */
    public List<Order> getOrders(int first, int size, OrdersFilter filter) {
        return (List<Order>) generateOrdersQuery(filter, false).setFirstResult(first).setMaxResults(size).list();
    }

    /**
     * Получить перечень заказов с учетом фильтра
     *
     * @param filter фильтр
     * @return перечень заказов
     * @see ru.niir.goodfil.utils.filters.OrdersFilter
     */
    public List<Order> getOrders(OrdersFilter filter) {
        return getOrders(0, Integer.MAX_VALUE, filter);
    }

    /**
     * Получить перечень заказов данного клиента
     */
    public List<Order> getOrdersByClientId(long clientId) {
        synchronized (this) {
            return (List<Order>) getSession().createQuery("from Order where clientId=? and isDeleted=?").setLong(0, clientId).setBoolean(1, false).list();
        }
    }

    /**
     * Получить перечень заказов, связянных с указанным менеджером
     */
    public List<Order> getOrdersByManagerId(long managerId) {
        synchronized (this) {
            return (List<Order>) getSession().createQuery("from Order where managerId=? and isDeleted=?").setLong(0, managerId).setBoolean(1, false).list();
        }
    }

    /**
     * Получить перечень элементов заказа по идентификатору заказа
     */
    public List<OrderItem> getOrderItemsByOrderId(long orderId) {
        return (List<OrderItem>) getSession().createQuery("from OrderItem where orderId=?").setLong(0, orderId).list();
    }

    /**
     * Получить количество позиций заказа по идентификатору заказа
     */
    public long getOrderItemsCountByOrderId(long orderId) {
        synchronized (this) {
            return (Long) getSession().createQuery("select count(*) from OrderItem where orderId=?").setLong(0, orderId).uniqueResult();
        }
    }

    /**
     * Метод устанавливает ожидаемую дату сборки исходя из условий, предлагаемых для разных клиентских групп.
     * Условия заключаются в различном количестве дней, в течение которых склад должен собрать заказ.
     * Эти условия нужно из жесткой привязки перенести как поля в таблицу settings - это добавит гибкости и облегчит
     * изменение этих условий.
     *
     * @param order
     */
    public void setOrderExpectedAssemblyDate(Order order) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(order.getChangeDate() == null ? order.getCreationDate() : order.getChangeDate());
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        int days = 0;

        // Если экспресс доставка или интернет-магазин день в день
        if (order.getClient().isClientStateIsEXPRESSDELIVERY() || order.getClient().isClientSateIsESHOPDAYTODAY()) {
            // Проверка, что не позднее 10 или 14 часов соостветственно. Если позднее, добавляем 1 день
/*            if(order.getCreationDate().after(calendar.getTime())) {
                if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY) {
                    calendar.add(Calendar.DATE, 3);
                } else if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
                    calendar.add(Calendar.DATE, 2);
                }
                else {
                    calendar.add(Calendar.DATE, 1);
                }
            }*/
        }

        //Если торговый дом
        else if (order.getClient().isClientSateIsTD()) {
            // Добавляем 4 рабочих дня
            days = getSettingByName("td.assembly.days").getValueAsInteger();
        }

        //Если все остальные (не особенные клиенты). Добавляем 1 раюочий день
        else {
            days = getSettingByName("clients.assembly.days").getValueAsInteger();

        }

        if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY) {
            days += 2;
        } else if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
            days += 1;
        }
        calendar.add(Calendar.DATE, days);
        order.setExpectedAssembyDate(calendar.getTime());
        if (!order.getClient().isClientStateIsEXPRESSDELIVERY()) {
            if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY) {
                calendar.add(Calendar.DATE, 3);
            } else if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
                calendar.add(Calendar.DATE, 2);
            } else {
                calendar.add(Calendar.DATE, 1);
            }
        }
        order.setExpectedDeliveryDate(calendar.getTime());
    }

    /**
     * ======================================================================================================================================================
     */

    //
    //  section Уведомления ok
    //

    /**
     * Получить перечень необработанных уведомлений
     */
    public List<Notification> getNewNotifications() {
        synchronized (this) {
            return (List<Notification>) getSession().createQuery("from Notification where notificationState=?").setInteger(0, NotificationState.NEW).list();
        }
    }

    //
    //  section Замещения ok
    //

    /**
     * Получить перечень замещений в системе
     */
    public List<Replacement> getReplacements() {
        synchronized (this) {
            return (List<Replacement>) getSession().createQuery("from Replacement").list();
        }
    }

    /**
     * Получить перечень замещений в системе, с учетом фильтра и разбиения на страницы
     *
     * @param filter параметры фильтрации
     */
    public List<Replacement> getReplacements(int first, int size, ReplacementsFilter filter) {
        QueryBuilder b = QueryBuilder.WithWhere("from Replacement");

        if (filter != null) {
            if (filter.getAssistanceId() != -1) {
                b.addWhereCondition("assistantId=?", filter.getAssistanceId());
            }
            if (filter.getManagerId() != -1) {
                b.addWhereCondition("managerId=?", filter.getManagerId());
            }
            if (filter.getDate() != null) {
                b.addWhereCondition("(begdate <= ? or begdate is null)", filter.getDate());
                b.addWhereCondition("(enddate >= ? or enddate is null)", filter.getDate());
            }
        }
        return (List<Replacement>) b.toQuery(getSession()).setFirstResult(first).setMaxResults(size).list();
    }

    /**
     * Получить перечень замещений в системе, с учетом фильтра и разбиения на страницы
     *
     * @param filter параметры фильтрации
     */
    public int getReplacementsCount(ReplacementsFilter filter) {
        QueryBuilder b = QueryBuilder.WithWhere("select count (*) from Replacement");

        if (filter != null) {
            if (filter.getAssistanceId() != -1) {
                b.addWhereCondition("assistantId=?", filter.getAssistanceId());
            }
            if (filter.getManagerId() != -1) {
                b.addWhereCondition("managerId=?", filter.getManagerId());
            }
            if (filter.getDate() != null) {
                b.addWhereCondition("(begdate <= ? or begdate is null)", filter.getDate());
                b.addWhereCondition("(enddate >= ? or enddate is null)", filter.getDate());
            }
        }
        return ((Long) b.toQuery(getSession()).uniqueResult()).intValue();
    }

    /**
     * Получить замещение, замещающего данного пользователя (по идентификатору)
     *
     * @param managerId идентификатор пользователя
     */
    public List<Replacement> getReplacementsByManagerId(long managerId) {
        synchronized (this) {
            return (List<Replacement>) getSession().createQuery("from Replacement where managerId=?").setLong(0, managerId).list();
        }
    }

    /**
     * Получить перечень замещений данного ассистента (по идентификатору)
     *
     * @param assistantId идентификатор пользователя
     */
    public List<Replacement> getReplacementsByAssistantId(long assistantId) {
        synchronized (this) {
            return (List<Replacement>) getSession().createQuery("from Replacement where assistantId=?").setLong(0, assistantId).list();
        }
    }

    /**
     * Возвращает перечень активных замещений по идентификатору ЗАМЕЩАЕМОГО.
     *
     * @param assistantId идентификатор замещаемого
     */
    public List<Replacement> getActiveReplacementsByManagerId(long managerId) {
        final List<Replacement> result = new ArrayList<Replacement>();
        final List<Replacement> replacements = getReplacementsByManagerId(managerId);

        for (Replacement replacement : replacements) {
            if (replacement.isActive()) {
                result.add(replacement);
            }
        }

        return result;
    }

    /**
     * Возвращает перечень активных замещений по идентификатору ЗАМЕЩАЮЩЕГО.
     *
     * @param assistantId идентификатор замещающего
     */
    public List<Replacement> getActiveReplacementsByAssistantId(long assistantId) {
        final List<Replacement> result = new ArrayList<Replacement>();
        final List<Replacement> replacements = getReplacementsByAssistantId(assistantId);

        for (Replacement replacement : replacements) {
            if (replacement.isActive()) {
                result.add(replacement);
            }
        }

        return result;
    }

    public Set<Long> getReplacedManagersByAssistantId(long assistantId) {
        final Set<Long> replacedManagersIds = new HashSet<Long>();
        if (assistantId > 0) {
            List<Replacement> replacements = getActiveReplacementsByAssistantId(assistantId);
            if (replacements != null) {
                for (Replacement replacement : replacements) {
                    replacedManagersIds.add(replacement.getManagerId());
                }
            }
        }
        return replacedManagersIds;
    }

    //
    //  section: Персональные цены ok
    //

    /**
     * Возвращает количество персональных цен, соответствующих фильтру
     *
     * @param filter фильтр персональных цен
     * @return количество персональных цен, соответствующих фильтру
     */
    public int getPersonalPricesCount(PersonalPricesFilter filter) {
        QueryBuilder b = QueryBuilder.WithoutWhere("select count(*) from PersonalPrice price, Good good, Client client where price.goodId = good.id and price.clientId = client.id");
        if (filter != null) {
            if (!isBlank(filter.getClientName())) {
                b.addWhereCondition("client.companyName like ?", "%" + filter.getClientName() + "%");
            }
            if (!isBlank(filter.getGoodName())) {
                b.addWhereCondition("good.name like ?", "%" + filter.getGoodName() + "%");
            }
            if (filter.getDate() != null) {
                b.addWhereCondition("(price.begDate<=? or price.begDate is null)", filter.getDate());
            }
            if (filter.getDate() != null) {
                b.addWhereCondition("(price.endDate>=? or price.endDate is null)", filter.getDate());
            }
            if (filter.getClientId() != -1) {
                b.addWhereCondition("price.clientId=?", filter.getClientId());
            }
            if (filter.getGoodId() != -1) {
                b.addWhereCondition("price.goodId=?", filter.getGoodId());
            }
        }


        Object result = b.toQuery(getSession()).uniqueResult();
        return ((Long) result).intValue();
    }

    /**
     * Получить персональные цены на сегодня на данный товар для данного клиента
     *
     * @param clientId идентификатор клиента
     * @param goodId   идентификатор товара
     * @return персональные цены на сегодня на данный товар для данного клиента
     */
    public List<PersonalPrice> getTodayPersonalPricesByGoodAndClient(long clientId, long goodId) {
        return getPersonalPrices(new PersonalPricesFilter(clientId, goodId, GoodfilUtils.getCurrentDate()));
    }

    /**
     * Возвращает все персональные цены, зарегистрированные в системе.
     */
    public List<PersonalPrice> getAllPersonalPrices() {
        return getPersonalPrices(null);
    }

    /**
     * Возвращает список персональных цен, соответствующих фильтру
     *
     * @param filter фильтр персональных цен
     * @return список персональных цен
     */
    public List<PersonalPrice> getPersonalPrices(PersonalPricesFilter filter) {
        QueryBuilder b = QueryBuilder.WithoutWhere("from PersonalPrice price, Good good, Client client where price.goodId = good.id and price.clientId = client.id");
        if (filter != null) {
            if (!isBlank(filter.getClientName())) {
                b.addWhereCondition("client.companyName like ?", "%" + filter.getClientName() + "%");
            }
            if (!isBlank(filter.getGoodName())) {
                b.addWhereCondition("good.name like ?", "%" + filter.getGoodName() + "%");
            }
            if (filter.getDate() != null) {
                b.addWhereCondition("(price.begDate<=? or price.begDate is null)", filter.getDate());
            }
            if (filter.getDate() != null) {
                b.addWhereCondition("(price.endDate>=? or price.endDate is null)", filter.getDate());
            }
            if (filter.getClientId() != -1) {
                b.addWhereCondition("price.clientId=?", filter.getClientId());
            }
            if (filter.getGoodId() != -1) {
                b.addWhereCondition("price.goodId=?", filter.getGoodId());
            }
        }

        List<Object[]> objs = b.toQuery(getSession()).list();
        List<PersonalPrice> result = new ArrayList<PersonalPrice>();
        for (Object[] obj : objs) {
            result.add((PersonalPrice) obj[0]);
        }
        return result;
    }

    /**
     * Возвращает список персональных цен, соответствующих фильтру, с разбиением на страницы
     *
     * @param start  индекс первого элемента страницы
     * @param page   размер страницы
     * @param filter фильтр персональных цен
     * @return список персональных цен
     */
    public List<PersonalPrice> getPersonalPrices(int start, int page, PersonalPricesFilter filter) {
        QueryBuilder b = QueryBuilder.WithoutWhere("from PersonalPrice price, Good good, Client client where price.goodId = good.id and price.clientId = client.id");
        if (filter != null) {
            if (!isBlank(filter.getClientName())) {
                b.addWhereCondition("client.companyName like ?", "%" + filter.getClientName() + "%");
            }
            if (!isBlank(filter.getGoodName())) {
                b.addWhereCondition("good.name like ?", "%" + filter.getGoodName() + "%");
            }
            if (filter.getDate() != null) {
                b.addWhereCondition("(price.begDate<=? or price.begDate is null)", filter.getDate());
            }
            if (filter.getDate() != null) {
                b.addWhereCondition("(price.endDate>=? or price.endDate is null)", filter.getDate());
            }
            if (filter.getClientId() != -1) {
                b.addWhereCondition("price.clientId=?", filter.getClientId());
            }
            if (filter.getGoodId() != -1) {
                b.addWhereCondition("price.goodId=?", filter.getGoodId());
            }
            if(filter.getManagerId() != -1){
                b.addWhereCondition("client.managerId=?", filter.getManagerId());
            }
        }

        List<Object[]> objs = b.toQuery(getSession()).setFirstResult(start).list();
        List<PersonalPrice> result = new ArrayList<PersonalPrice>();
        for (Object[] obj : objs) {
            result.add((PersonalPrice) obj[0]);
        }
        return result;
    }

    //
    //  section Прейскуранты (вторая реализация) ok
    //

    /**
     * Получить цену на данный товар для данного клиента с учетом механизма ценообразования. Если параметр clientId
     * будет иметь значение -1, то будет возвращена цена на товар для незарегистрированного клиента, т.е. самая высокая
     * цена, цена самой "дорогой" клиентской группы.
     * <p/>
     * МОДИФИКАЦИЯ от 8 февраля 2010г: клиентская группа теперь берется не у клиента, а указывается отдельно (третьим
     * параметром). Это связано с тем, что расширенный алгоритм определения клиентской группы требует сложных операций
     * для вычисления клиентской группы клиента.
     *
     * @param goodId   идентификатор товара
     * @param clientId идентификатор клиента. Если параметр будет иметь значение -1, то будет возвращена цена на товар
     *                 для незарегистрированного клиента, т.е. самая высокая цена, цена самой "дорогой" клиентской
     *                 группы.
     * @return объект PriceInfo, содержащий помимо цены информацию о том, в результате каких операций была получена
     * данная цена
     * @see ru.niir.goodfil.utils.PriceInfo
     */
//    public PriceInfo getPrice(long goodId, long clientId, long clientGroupId)
//    {
//        synchronized (this)
//        {
//            List<PersonalPrice> personalPrices = getTodayPersonalPricesByGoodAndClient(clientId, goodId);
//            if (personalPrices.size() > 0)
//            {
//                if (personalPrices.size() > 1)
//                {
//                    logger.error("Error in personal prices, we have more than 1 price for: clientId=" + clientId + ", goodId=" + goodId);
//                    throw new PersonalPricesException(PersonalPricesError.INTRESECT);
//                }
//
//                float price = (float) personalPrices.get(0).getPrice();
//                return new PriceInfo(goodId, clientId, -1, true, price);
//            }
//            else
//            {
//                if (clientId == -1) clientGroupId = getMainClientGroupId();
//
//                SimplePricelist simplePricelist = (SimplePricelist) getSession().createQuery("from SimplePricelist where goodId=? and clientGroupId=?").setLong(0, goodId).setLong(1, clientGroupId).uniqueResult();
//
//                _assertNotNull(simplePricelist, "Либо goodId (" + goodId + ") неверен, либо clientGroupId (" + clientGroupId + ") неверен, либо ошибка в механизме формирования прайслиста");
//
//                float price = simplePricelist == null ? -1 : simplePricelist.getPrice();
//                return new PriceInfo(goodId, clientId, clientGroupId, false, price);
//            }
//        }
//    }
    public PriceInfo getPrice2(Good good, Client client) {
        _assertNotNull(good);
        _assertNotNull(client);

        return getPrice2(good.getId(), good.getBrandId(), client.getId(), client.getClientgroupIdByBrandId(good.getBrandId()));
    }

    public PriceInfo getPrice2(long goodId, long brandId, long clientId, long clientGroupId) {
        synchronized (this) {
            List<PersonalPrice> personalPrices = getTodayPersonalPricesByGoodAndClient(clientId, goodId);
            if (personalPrices.size() > 0) {
                if (personalPrices.size() > 1) {
                    logger.error("Error in personal prices, we have more than 1 price for: clientId=" + clientId + ", goodId=" + goodId);
                    throw new PersonalPricesException(PersonalPricesError.INTRESECT);
                }

                float price = (float) personalPrices.get(0).getPrice();
                return new PriceInfo(goodId, clientId, -1, true, price);
            } else {
                if (clientId == -1) {
                    clientGroupId = getMainClientGroupId2(brandId);
                }

                SimplePricelist simplePricelist = (SimplePricelist) getSession().createQuery("from SimplePricelist where goodId=? and clientGroupId=?").setLong(0, goodId).setLong(1, clientGroupId).uniqueResult();

                _assertNotNull(simplePricelist, "Либо goodId (" + goodId + ") неверен, либо clientGroupId (" + clientGroupId + ") неверен, либо ошибка в механизме формирования прайслиста");

                float price = simplePricelist == null ? -1 : simplePricelist.getPrice();
                return new PriceInfo(goodId, clientId, clientGroupId, false, price);
            }
        }
    }

    /**
     * Функция определения клиентской группы клиента с учетом расширенного алгоритма определения клиентской группы.
     * Расширенный алгоритм определения клиентской группы:
     * <p/>
     * <p/>
     * АЛГОРИТМ АВТОМАТИЧЕСКОГО ОПРЕДЕЛЕНИЯ КЛИЕНТСКОЙ ГРУППЫ ДЛЯ НОВЫХ КЛИЕНТОВ, ПРОШЕДШИХ РЕГИСТРАЦИЮ
     * <p/>
     * УСЛОВИЕ 1 Клиент считается новым если: 1. Клиент полностью прошел процедуру регистрации, ему назначен курирующий
     * менеджер 2. Клиент отнесен к первоначальной клиентской группе (группа с наименьшим объемом закупок и,
     * соответственно, с максимальными ценами) 3. C момента регистрации клиента прошло менее 90 дней (3-х месяцев)
     * Пояснение: К клиенту, зарегистрированному в системе более 90 дней назад или отнесенному вручную к клиентской
     * группе, отличной от минимальной, данный алгоритм не применяется.
     * <p/>
     * УСЛОВИЕ 2 В качестве ранее оформленных клиентом заказов, используемых для оценки объема закупок, используются
     * только заказы оплаченные заказы (статус "оплачен" и далее).
     * <p/>
     * АЛГОРИТМ
     * <p/>
     * При оформлении очередного заказа клиенту, удовлетворяющего условию 1, предлагаются цены клиентской группы с
     * суммой обязательных закупок максимально близкой или равной, сумме заказов этого клиента, удовлетворяющих условию
     * 2.
     * <p/>
     * Вопрос: как поступать, если, например, сегодня 5-е число месяца, 10-е, 15-е, а у клиента - случай 1: нет истории
     * закупок за предыдущий месяц (клиент зарегистрирован в текущем месяце); - случай 2: есть история закупок истории
     * нет: в этом случае берем данные за текущий месяц и так поступаем до последнего числа месяца включительно история
     * есть: в этом случае берем данные за предыдущий месяц, даже если клиент зарегистрирован в его конце или середине
     *
     * @param clientId идентификатор клиента
     * @return клиентская группа клиента
     */
//    public long getClientGroupByClientId(long clientId)
//    {
//        final long mainClientGroupId = getMainClientGroupId();
//
//        //  Если идентификатор клиента не указан (-1), то возвращаем самую дорогую клиентскую группу
//        if (clientId == -1 || clientId == 0) return mainClientGroupId;
//
//        // Нужно понять, является-ли клиент новым
//        boolean thisClientIsNew;
//
//        java.sql.Timestamp ts = (java.sql.Timestamp) getSession().createQuery("select c.creationDate from Client c where c.id=?").setLong(0, clientId).uniqueResult();
//        long days = differenceInDays(ts.getTime(), Calendar.getInstance().getTimeInMillis());
//
//
//        Long clientGroupId = (Long) getSession().createQuery("select c.clientGroupId from Client c where c.id=?").setLong(0, clientId).uniqueResult();
//        _assert(clientGroupId != null, "Клиентская группа не может иметь значение NULL");
//
//
//        //  Клиент считается новым, если с момента регистрации прошло менее 90 дней И
//        //  он отнесен первоначальной клиентской группе (группа с наименьшим объемом закупок и,
//        //  соответственно, с максимальными ценами)
//        thisClientIsNew = (( days <= 30*3 ) &&
//                           ( clientGroupId == mainClientGroupId ));
//
//        if (thisClientIsNew)
//        {   //  Реализация расширенного алгоритма определения клинентской группы
//
//            //  Здесь мы вычисляем объем закупок. Для этого смотрим, есть ли у клиента
//            //  история закупок за предыдущий календарный месяц.
//            float sum = countOrdersSumByClientAndPeriod(clientId,
//                                                        Utils.getBeginOfPreviousMonth().getTimeInMillis(),
//                                                        Utils.getEndOfPreviousMonth().getTimeInMillis());
//
//
//            //  Если за предыдущий месяц оплаченых заказов нет, считаем оплаченые заказы за текущий меясц
//            if (sum == 0) sum = countOrdersSumByClientAndPeriod(clientId,
//                                                                Utils.getBeginOfCurrentMonth().getTimeInMillis(),
//                                                                Utils.getEndOfCurrentMonth().getTimeInMillis());
//
//            //  И, наконец, выбираем подходящую клиентскую группу
//            return getClientGroupIdBySum(sum);
//
//        }
//        else
//        {   //  Реализация стандартного алгоритма определения клиентской группы
//            return clientGroupId;
//        }
//    }
    public long getClientGroupByClientId2(long clientId, long brandId) {
        final long mainClientGroupId = getMainClientGroupId2(brandId);

        //  Если идентификатор клиента не указан (-1), то возвращаем самую дорогую клиентскую группу
        if (clientId == -1 || clientId == 0) {
            return mainClientGroupId;
        }

        // Нужно понять, является-ли клиент новым
        boolean thisClientIsNew;

        java.sql.Timestamp ts = (java.sql.Timestamp) getSession().createQuery("select c.creationDate from Client c where c.id=?").setLong(0, clientId).uniqueResult();
        long days = differenceInDays(ts.getTime(), Calendar.getInstance().getTimeInMillis());


        BigInteger ClientGroupId = (BigInteger) getSession().createSQLQuery("select c.IdClientGroup from ClientsClientgroups as c where c.IdClient = ? and c.IdBrand = ?")
                .setLong(0, clientId)
                .setLong(1, brandId)
                .uniqueResult();
        long clientGroupId = ClientGroupId == null ? mainClientGroupId : ClientGroupId.longValue();

        //  Клиент считается новым, если с момента регистрации прошло менее 90 дней И
        //  он отнесен первоначальной клиентской группе (группа с наименьшим объемом закупок и,
        //  соответственно, с максимальными ценами)
        thisClientIsNew = ((days <= 30 * 3) &&
                (clientGroupId == mainClientGroupId));

        if (thisClientIsNew) {   //  Реализация расширенного алгоритма определения клинентской группы

            //  Здесь мы вычисляем объем закупок. Для этого смотрим, есть ли у клиента
            //  история закупок за предыдущий календарный месяц.
            float sum = countOrdersSumByClientAndPeriod2(getSession(),
                    clientId,
                    brandId,
                    Utils.getBeginOfPreviousMonth().getTimeInMillis(),
                    Utils.getEndOfPreviousMonth().getTimeInMillis());


            //  Если за предыдущий месяц оплаченых заказов нет, считаем оплаченые заказы за текущий меясц
            if (sum == 0) {
                sum = countOrdersSumByClientAndPeriod2(getSession(),
                        clientId,
                        brandId,
                        Utils.getBeginOfCurrentMonth().getTimeInMillis(),
                        Utils.getEndOfCurrentMonth().getTimeInMillis());
            }

            //  И, наконец, выбираем подходящую клиентскую группу
            return getClientGroupIdBySum2(brandId, sum);
        } else {   //  Реализация стандартного алгоритма определения клиентской группы
            return clientGroupId;
        }
    }

    /**
     * Возвращает сумму оплаченых заказов для данного клиента за указаный период.
     *
     * @param clientId идентификатор клиента
     * @param begdate  дата начала отчетного периода
     * @param enddate  дата окончания отчетного периода
     * @return сумма оплаченых заказов для данного клиента за указаный период
     */
//    private float countOrdersSumByClientAndPeriod(long clientId, long begdate, long enddate)
//    {
//        _assert(clientId != -1 && clientId != 0, "Не существующий идентификатор клиента");
//
//        OrdersFilter ordersFilter = new OrdersFilter();
//        ordersFilter.setClientId(clientId);
//        ordersFilter.setBegDate(new java.sql.Date(begdate));
//        ordersFilter.setEndDate(new java.sql.Date(enddate));
//
//        float sum = 0;  //  Сумма оплаченых заказов за предыдущий месяц
//        List<Order> orders = getOrders(ordersFilter);
//        for (Order order : orders)
//        {
//            //  Нас интересуют только оплаченые заказы
//            if (order.getOrderStateId() >= OrderState.PAYED && order.getOrderStateId() <= OrderState.COMPLETE)
//            {
//                countOrderSum(order);       //  Считаем стоимость заказа без доставки
//                sum += order.getSum();      //  Добавляем стоимость заказа к сумме закупок за предыдущий меясц
//            }
//        }
//        return sum;
//    }
    private static float countOrdersSumByClientAndPeriod2(StatelessSession session, long clientId, long brandId, long begdate, long enddate) {
        _assert(clientId > 0, "Не существующий идентификатор клиента");

        String query = "select SUM(oi.`Count` * (IF(oi.`StartPrice` > oi.`ManualPrice`, oi.`StartPrice`, oi.`ManualPrice`))) as `Price` " +
                "from OrderItems as oi, Orders as o, Goods as g " +
                "where " +
                "oi.`IdOrder` = o.`Id` and " +
                "oi.`IdGood` = g.`Id` and " +
                "o.`IdClient` = ? and " +
                "g.`IdBrand` = ? and " +
                "o.`IsDeleted` = 0 and " +
                "o.`CreationDate` BETWEEN ? and ?";

        Double result = (Double) session.createSQLQuery(query)
                .setLong(0, clientId)
                .setLong(1, brandId)
                .setDate(2, new java.sql.Date(begdate))
                .setDate(3, new java.sql.Date(enddate))
                .uniqueResult();

        return result == null ? 0 : result.floatValue();
    }

    /**
     * Функция возвращает все цены для товара с указанным идентификатором. Цены возвращаются в виде словара, где ключ -
     * идентификатор клиентской группы, а значение - цена.
     *
     * @param goodId идентификатор товара
     * @return все цены для указанного товара
     */
    public Map<Long, Float> getPricesMap(long goodId) {
        synchronized (this) {
            Map<Long, Float> result = new HashMap<Long, Float>();
            List<SimplePricelist> simplePricelists = (List<SimplePricelist>) getSession().createQuery("from SimplePricelist where goodId=?").setLong(0, goodId).list();
            int counter = 0;
            for (SimplePricelist simplePricelist : simplePricelists) {
                result.put(simplePricelist.getClientGroupId(), simplePricelist.getPrice());
                counter++;
            }
            return result;
        }
    }

    /**
     * Добавить к товару информация о всех ценах
     *
     * @param good товар
     */
    public void addPricesToGood(Good good) {
        if (good != null) {
            Map<Long, Float> r1 = getPricesMap(good.getId());
            Iterator i = r1.keySet().iterator();
            while (i.hasNext()) {
                long key = (Long) i.next();
                good.getPricesMap().put(key, r1.get(key));
            }
        }
    }

    /**
     * Получить записи "простого" прайслиста
     *
     * @param first  индекс первой записи (для разбиения на страницы)
     * @param size   количество записей на странице (для разбиения на страницы)
     * @param filter фильтр
     */
    public List<PricelistItem> getPricelist(int first, int size, GoodsFilter filter) {
        logger.debug("getPricelist");
        logger.debug("  first  : " + first);
        logger.debug("  size   : " + size);
        logger.debug("  fitler : " + filter);

        QueryBuilder b = QueryBuilder.WithoutWhere("from Good g, GoodType goodType where g.goodTypeId = goodType.id and g.deleted=false");

        if (filter != null) {
            b.addWhereCondition("g.code like ?", "%" + filter.getCode() + "%");
            b.addWhereCondition("g.name like ?", "%" + filter.getName() + "%");
            if (filter.getGoodTypeId() != -1) {
                b.addWhereCondition("g.goodTypeId = ?", filter.getGoodTypeId());
            }
        }

        List res1;
        synchronized (this) {
            res1 = b.toQuery(getSession())
                    .setFirstResult(first)
                    .setMaxResults(size)
                    .list();
        }

        List<PricelistItem> result = new ArrayList<PricelistItem>();
        for (Object aRes1 : res1) {
            Object[] obj = (Object[]) aRes1;
            Good good = (Good) obj[0];
            GoodType goodType = (GoodType) obj[1];
            good.setGoodType(goodType);

            List<SimplePricelist> prices;
            synchronized (this) {
                prices = getSession().createQuery("from SimplePricelist price where price.goodId = ?")
                        .setLong(0, good.getId())
                        .list();
            }

            PricelistItem pricelistItem = new PricelistItem(good);
            for (SimplePricelist price : prices) {
                pricelistItem.getPrices().put(price.getClientGroupId(), new FloatPrice(price.getPrice()));
            }
            result.add(pricelistItem);
        }

        Collections.sort(result, new Comparator<PricelistItem>() {
            @Override
            public int compare(PricelistItem o1, PricelistItem o2) {
                return o1.getGood().getCode().compareTo(o2.getGood().getCode());
            }
        });
        return result;
    }

    /**
     * Обновить строчку прейскуранта
     */
    public void updatePricelistItem(PricelistItem pricelistItem) throws ValidationException {
        assert pricelistItem != null;

        Transaction tx = getSession().beginTransaction();
        try {
            for (long clientGroupId : pricelistItem.getPrices().keySet()) {
                FloatPrice floatPrice = pricelistItem.getPrices().get(clientGroupId);
                if (!floatPrice.hasChanged()) {
                    continue;
                }

                long goodId = pricelistItem.getGood().getId();

                if (!exists(Good.class, goodId)) {
                    throw new ValidationException(SimplePricelist.class, "goodId", "Товар", goodId, ValidationProblem.REFERENCE);
                }
                if (!exists(ClientGroup.class, clientGroupId)) {
                    throw new ValidationException(SimplePricelist.class, "clientGroupId", "Клиенсткая группа", clientGroupId, ValidationProblem.REFERENCE);
                }

                float price = floatPrice.getValue();
                if (price < 0) {
                    throw new ValidationException(SimplePricelist.class, "price", "Цена", price, ValidationProblem.POSITIVE_ZERO);
                }

                int updated = getSession().createQuery("update SimplePricelist set price=? where goodId=? and clientGroupId=?")
                        .setFloat(0, price)
                        .setLong(1, pricelistItem.getGood().getId())
                        .setLong(2, clientGroupId)
                        .executeUpdate();

                logger.debug("Updating price for good " + pricelistItem.getGood().getId() + " and clientGroup " + clientGroupId + ": " + updated);
            }

            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

//    /**
//     * Тобавить товар в прейскурант
//     */
//    public void addGoodToPricelist(long goodId)
//    {
//        if (!exists(Good.class, goodId)) throw new InvalidIdException(Good.class, goodId);
//
//        logger.debug("Adding good " + goodId + " to pricelist");
//
//        if(0 < (Long) getSession().createQuery("select count (*) from SimplePricelist where goodId=?").setLong(0, goodId).uniqueResult())
//            removeGoodFromPricelist(goodId);
//
//        Transaction tx = getSession().beginTransaction();
//        try
//        {
//            for (ClientGroup clientGroup : getClientGroups()) getSession().insert(new SimplePricelist(goodId, clientGroup.getId(), 0));
//            tx.commit();
//        }
//        finally
//        {
//            if (!tx.wasCommitted()) tx.rollback();
//        }
//    }
//
//    /**
//     * Удалить товар из прейскуранта
//     */
//    public void removeGoodFromPricelist(long goodId)
//    {
//        if (!exists(Good.class, goodId)) throw new InvalidIdException(Good.class, goodId);
//
//        logger.debug("Removing good " + goodId + " from pricelist");
//        Transaction tx = getSession().beginTransaction();
//        try
//        {
//            int deleted = getSession().createQuery("delete from SimplePricelist where goodId=?").setLong(0, goodId).executeUpdate();
//            logger.debug("..deleted: " + deleted);
//            tx.commit();
//        }
//        finally
//        {
//            if (!tx.wasCommitted()) tx.rollback();
//        }
//    }

//    /**
//     * Добавить клиентскую группу в прейскурант
//     */
//    public void addClientGroupToPricelist(long clientGroupId)
//    {
//        if (!exists(ClientGroup.class, clientGroupId)) throw new InvalidIdException(ClientGroup.class, clientGroupId);
//
//        logger.debug("Adding client group " + clientGroupId + " to pricelist");
//        long count = (Long) getSession().createQuery("select count(*) from Good where isDeleted=false").uniqueResult();
//        for (int beg = 0; beg < count; beg += 100)
//        {
//            Transaction tx = getSession().beginTransaction();
//
//            try
//            {
//                List result = getSession().createQuery("select id from Good where isDeleted=false").setFirstResult(beg).setMaxResults(100).list();
//                for (Object aResult : result)
//                {
//                    long goodId = (Long) aResult;
//                    getSession().insert(new SimplePricelist(goodId, clientGroupId, 0));
//                }
//
//                tx.commit();
//            }
//            finally
//            {
//                if (!tx.wasCommitted()) tx.rollback();
//            }
//        }
//    }
//
//    /**
//     * Удалить клиентскую группу из прейскуранта
//     */
//    public void removeClientGroupFromPricelist(long clientGroupId)
//    {
//        if (!exists(ClientGroup.class, clientGroupId)) throw new InvalidIdException(ClientGroup.class, clientGroupId);
//
//        logger.debug("Removing client group " + clientGroupId + " from pricelist");
//        Transaction tx = getSession().beginTransaction();
//        try
//        {
//            getSession().createQuery("delete from SimplePricelist where clientGroupId=?").setLong(0, clientGroupId).executeUpdate();
//            tx.commit();
//        }
//        finally
//        {
//            if (!tx.wasCommitted()) tx.rollback();
//        }
//    }

    /**
     * Создать новый "простой" прейскурант на основе текущего переченя товаров TODO: Подумать над безопасностью этой
     * функции
     */
    public void createSimplePricelist() {
        /*
            При создании простого прейскуранта мы игнорируем фиктивно-удаленные товары
         */

        logger.debug("Creating simple pricelist");

        Transaction tx = getSession().beginTransaction();
        getSession().createQuery("delete from SimplePricelist").executeUpdate();
        tx.commit();

        long count = (Long) getSession().createQuery("select count(*) from Good where isDeleted=false").uniqueResult();
        logger.debug("  goods: " + count);

        List<ClientGroup> clientGroups = getClientGroups();

        for (int beg = 0; beg < count; beg += 100) {
            logger.debug("Proceeding from " + beg);
            tx = getSession().beginTransaction();

            List result = getSession().createQuery("select id from Good where isDeleted=false").setFirstResult(beg).setMaxResults(100).list();
            for (Object aResult : result) {
                long goodId = (Long) aResult;
                for (ClientGroup clientGroup : clientGroups) {
                    getSession().insert(new SimplePricelist(goodId, clientGroup.getId(), 0));
                }
            }

            tx.commit();
        }

        logger.debug("Finished");
    }

    /**
     * Проверить прейскурант на наличие ошибок
     *
     * @return true, если ошибок нет. если ошибки есть - смотри логи
     */
    public boolean checkPricelist() {
        synchronized (this) {
            boolean r = true;
            List<ClientGroup> clientGroups = getClientGroups();

            logger.debug("Checking goods");

            long count = (Long) getSession().createQuery("select count(*) from Good where isDeleted=false").uniqueResult();
            for (int beg = 0; beg < count; beg += 100) {
                List result = getSession().createQuery("select id from Good where isDeleted=false").setFirstResult(beg).setMaxResults(100).list();
                for (Object aResult : result) {
                    long goodId = (Long) aResult;

                    for (ClientGroup clientGroup : clientGroups) {
                        long clientGroupId = clientGroup.getId();

                        long c = (Long) getSession().createQuery("select count(*) from SimplePricelist where goodId=? and clientGroupId=?")
                                .setLong(0, goodId)
                                .setLong(1, clientGroupId)
                                .uniqueResult();

                        if (c == 0) {
                            logger.error("Missing line for good " + goodId + " and clientGroup " + clientGroupId);
                            logger.error("  ..good        : " + getGoodById(goodId));
                            logger.error("  ..clientGroup : " + getClientGroupById(clientGroupId));
                            r = false;
                        } else if (c > 1) {
                            logger.error("More than one line for good " + goodId + " and clientGroup " + clientGroupId);
                            logger.error("  ..good        : " + getGoodById(goodId));
                            logger.error("  ..clientGroup : " + getClientGroupById(clientGroupId));
                            r = false;
                        } else {
                            logger.debug("Ok for good " + goodId + " and clientGroup " + clientGroupId);
                        }
                    }
                }
            }

            logger.debug("Checking SimplePricelist...");

            count = (Long) getSession().createQuery("select count(*) from SimplePricelist").uniqueResult();
            for (int beg = 0; beg < count; beg += 100) {
                List result = getSession().createQuery("select id, goodId, clientGroupId from SimplePricelist").setFirstResult(beg).setMaxResults(100).list();
                for (Object aResult : result) {
                    Object[] obj = (Object[]) aResult;
                    long id = (Long) obj[0];
                    long goodId = (Long) obj[1];
                    long clientGroupId = (Long) obj[2];

                    if (getGoodById(goodId) == null) {
                        logger.error("Bad goodId (" + goodId + ") for SimplePricelist " + id);
                        r = false;
                    } else if (getGoodById(goodId).isDeleted()) {
                        logger.error("Good with goodId (" + goodId + ") is deleted for SimplePricelist " + id);
                        r = false;
                    } else if (getClientGroupById(clientGroupId) == null) {
                        logger.error("Bad clientGroup (" + clientGroupId + ") for SimplePricelist " + id);
                        r = false;
                    } else {
                        logger.debug("Ok for SimplePricelist " + id);
                    }
                }
            }

            return r;
        }
    }

    /**
     * Получить количество записей в таблице SimplePricelist
     */
    public long getSimplePricelistLinesCount() {
        return (Long) getSession().createQuery("select count(*) from SimplePricelist").uniqueResult();
    }

    /**
     * Сгенерировать запрос к таблице прайслиста
     *
     * @param filter фильтр для прайслиста
     * @param mode   0 - запрос count(*), 1 - запрос select *, 2 - запрос select id
     * @return запрос (hibernate query)
     */
    private Query generateSimplePricelistQuery(SimplePricelistFilter filter, int mode) {
        assert mode == 0 || mode == 1 || mode == 2;

        QueryBuilder b = null;
        if (mode == 0) {
            b = QueryBuilder.WithWhere("select count(*) from SimplePricelist p");
        }
        if (mode == 1) {
            b = QueryBuilder.WithWhere("from SimplePricelist p");
        }
        if (mode == 2) {
            b = QueryBuilder.WithWhere("select p.id from SimplePricelist p");
        }

        assert b != null;

        if (filter != null) {
            logger.debug("Users filter: " + filter);
            if (filter.getGoodId() != -1) {
                b.addWhereCondition("p.goodId=?", filter.getGoodId());
            }
            if (filter.getClientGroupId() != -1) {
                b.addWhereCondition("p.clientGroupId=?", filter.getClientGroupId());
            }
            if (filter.getZeroPrice() != 0) {
                b.addWhereCondition("p.price=0");
            }
        }

        return b.toQuery(getSession());
    }

    /**
     * Возвращает число записей в таблице прейскуранта в соответствии с фильтром
     *
     * @param filter фильтр прайса
     * @return число записей в таблице прейскуранта в соответствии с фильтром
     */
    public long getSimplePricelistLinesCount(SimplePricelistFilter filter) {
        return (Long) generateSimplePricelistQuery(filter, 0).uniqueResult();
    }

    /**
     * Получить список записей в таблице прайса с разбиением на страницы с учетом фильтра
     *
     * @param first  индекс первого возвращаемого элемента
     * @param size   размер страницы
     * @param filter фильтр прайса
     * @return список записей в таблице прайса с разбиением на страницы с учетом фильтра
     */
    public List<SimplePricelist> getSimplePricelistLines(int first, int size, SimplePricelistFilter filter) {
        return (List<SimplePricelist>) generateSimplePricelistQuery(filter, 1).setFirstResult(first).setMaxResults(size).list();
    }

    /**
     * Получить перечень идентификаторов из таблицы прайса с разбиением на страницы с учетом фильтра
     *
     * @param first  индекс первого возвращаемого элемента
     * @param size   размер страницы
     * @param filter фильтр прайса
     * @return перечень идентификаторов из таблицы прайса с разбиением на страницы с учетом фильтра
     */
    public List<Long> getSimplePricelistLinesIds(int first, int size, SimplePricelistFilter filter) {
        return (List<Long>) generateSimplePricelistQuery(filter, 2).setFirstResult(first).setMaxResults(size).list();
    }

    public SimplePricelist getSimplePriceListByIdGoodAndGroupId(long goodId, long groupId){
        return (SimplePricelist) getSession().createQuery("from SimplePricelist s where s.goodId = ? and s.clientGroupId = ?").setParameter(0, goodId).setParameter(1, groupId).uniqueResult();
    }

    //
    //  section Работа с текстовыми блоками ok
    //

    /**
     * Получить текстовый блок по наименованию
     *
     * @param key наименование текстового блока
     * @return текстовый блок
     */
    public TextBlock getTextBlockByKey(String key) {
        key = s(key);
        synchronized (this) {
            return (TextBlock) getSession().createQuery("from TextBlock where key=?").setString(0, key).uniqueResult();
        }
    }

    public List<TextBlock> getTextBlocks() {
        synchronized (this) {
            return getSession().createQuery("from TextBlock").list();
        }
    }

    public void updateTextBlockValue(String id, String text) {
        synchronized (this) {
            Transaction tx = getSession().beginTransaction();
            try {
                getSession().createQuery("update TextBlock set value=? where key=?").setString(0, text).setString(1, id).executeUpdate();
                tx.commit();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (!tx.wasCommitted()) {
                    tx.rollback();
                }
            }
        }
    }


    //
    //  section Взаимодействие с 1С ok
    //

    public boolean getC1ErrorExists() {
        logger.debug("getC1ErrorExists() call");

        createIfNotExists(".1c.directory", "/media/1c", "Путь к директории для обмена данными с 1С");
        String c1dir = get(".1c.directory");

        if (c1dir == null) {
            logger.error("c1dir is null");
            return true;
        }

        File f = new File(c1dir + File.separator +
                "errors" + File.separator +
                "last_error.txt");
        logger.debug("Searching for " + f.getPath());

        logger.debug("File (" + f.getPath() + ") " + (f.exists() ? "exists" : "not exists"));
        return f.exists();
    }

    /**
     * Добавить объект DataExchange в БД
     *
     * @param dataExchange объект DataExchange
     * @throws ValidationException в случае, если добавляемый объект содержит ошибки
     */
    public void addDataExchange(DataExchange dataExchange) throws ValidationException {
        assert dataExchange != null;
        dataExchange.setExchangeStateId(ExchangeState.QUEUED);
        dataExchange.setCreationDate(GoodfilUtils.getCurrentTimestamp());

        if (isBlank(dataExchange.getTableName())) {
            throw new ValidationException(DataExchange.class, "tableName", "Наименование таблицы", dataExchange.getTableName(), ValidationProblem.NOT_EMPTY);
        }

        if (getC1ErrorExists()) {
            return;
        }

        Transaction tx = getSession().beginTransaction();
        try {
            getSession().insert(dataExchange);
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    /**
     * Получить перечень объектов DataExchange, соответствующих указанной таблице
     *
     * @param tableName наименование таблицы
     * @return перечень объектов DataExchange, соответствующих указанной таблице
     */
    public List<DataExchange> getDataExchanges(String tableName) {
        assert tableName != null;
        tableName = tableName.trim();
        assert !isBlank(tableName);

        synchronized (this) {
            return (List<DataExchange>) getSession().createQuery("from DataExchange where exchangeStateId=? and tableName=?").setInteger(0, ExchangeState.QUEUED).setString(1, tableName).list();
        }
    }

    /**
     * Получить информацию о последних 10 выгрузках. Это используется только в интерфейсе.
     */
    public List<DataExchange> getTopDataExchanges() {
        synchronized (this) {
            return (List<DataExchange>) getSession().createQuery("from DataExchange order by id desc").setMaxResults(50).list();
        }
    }

    public List<DataExchange> getTopDataExchanges(int count) {
        synchronized (this) {
            return (List<DataExchange>) getSession().createQuery("from DataExchange order by id desc").setMaxResults(count).list();
        }
    }


    /**
     * Функция получает первую выгрузку по статусом "в очереди"
     */
    public DataExchange getNextDataExchange() {
        synchronized (this) {
            List<DataExchange> dataExchanges = getSession().createQuery("from DataExchange where exchangeStateId=? order by id asc")
                    .setMaxResults(2)
                    .setInteger(0, ExchangeState.QUEUED)
                    .list();
            return dataExchanges.size() == 0 ? null : dataExchanges.get(0);
        }
    }

    /**
     * Функция возвращает перечень выгрузок не принятых 1С. Должна использоваться только в процедуре восстановления
     * работоспособности обмена
     */
    public List<DataExchange> getErrorDataExchanges() {
        synchronized (this) {
            return (List<DataExchange>) getSession().createQuery("from DataExchange where exchangeStateId=? order by id asc")
                    .setInteger(0, ExchangeState.SENT)
                    .list();
        }
    }
//
//    public DataExchange getLastDataExchange()
//    {
//        synchronized (this)
//        {
//            List<DataExchange> dataExchanges = getSession().createQuery("from DataExchange order by id asc").setMaxResults(2).list();
//            return dataExchanges.size() == 0 ? null : dataExchanges.get(0);
//        }
//    }

    /**
     * Обновить статус для указанного задания на обмен с 1С
     *
     * @param dataExchangeId    идентификатор задания на обмен с 1С
     * @param dataExchangeState новый статус
     */
    public void updateDataExchangeState(long dataExchangeId, int dataExchangeState) {
        _assert(dataExchangeState == ExchangeState.ACCEPTED ||
                dataExchangeState == ExchangeState.QUEUED ||
                dataExchangeState == ExchangeState.SENT);

        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createQuery("update DataExchange set exchangeStateId=? where id=?").setInteger(0, dataExchangeState).setLong(1, dataExchangeId).executeUpdate();
            tx.commit();
        } catch (Exception e) {
            logger.error(e);
            e.printStackTrace();

            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    public void closeDataExchange(long id) {
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createQuery("update DataExchange set exchangeStateId=? where id=?").setInteger(0, ExchangeState.ACCEPTED).setLong(1, id).executeUpdate();
            tx.commit();
        } catch (Exception e) {
            logger.error(e);
            e.printStackTrace();

            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    public void removeDataExchange(long id) {
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createQuery("delete from DataExchange where id = ?").setLong(0, id).executeUpdate();
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    //
    //  section методы, связанные с очисткой БД
    //

    public void clearGoodsData() {
        logger.warn("CLEARING GOODS DATA");
        Transaction tx = getSession().beginTransaction();

        try {
            getSession().createQuery("delete from OrderItem").executeUpdate();
            getSession().createQuery("delete from Order").executeUpdate();
            getSession().createQuery("delete from SimplePricelist").executeUpdate();
            getSession().createQuery("delete from PersonalPrice").executeUpdate();
            getSession().createQuery("delete from Good").executeUpdate();

            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    public void clearAllData() {
        logger.warn("CLEARING ALL DATA");
        Transaction tx = getSession().beginTransaction();

        try {
            getSession().createQuery("delete from OrderItem").executeUpdate();
            getSession().createQuery("delete from Order").executeUpdate();
            getSession().createQuery("delete from SimplePricelist").executeUpdate();
            getSession().createQuery("delete from PersonalPrice").executeUpdate();
            getSession().createQuery("delete from Client").executeUpdate();
            getSession().createQuery("delete from User where id!=1").executeUpdate();
            getSession().createQuery("delete from DataExchange").executeUpdate();
            getSession().createQuery("delete from Notification").executeUpdate();
            getSession().createQuery("delete from Good").executeUpdate();

            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    //
    //  section Реализация товарного кредита
    //

    public boolean clientCanSaveOrder(long clientId) throws SettingNotFoundException {
        createIfNotExists("tk.enabled", String.valueOf(1), "Механизм товарного кредита включен");
        int v = getInt("tk.enabled");

        if (v == 0) {
            return true;
        }

        Client client = getClientById(clientId);
        if (client == null) {
            return false;
        }

        float a = client.getDebtBank() + client.getDebtCash();
        List<Order> orders = getOrdersByClientId(clientId);
        for (Order order : orders) {
            if (order.getOrderStateId() != OrderState.PRE_SAVED &&
                    order.getOrderStateId() != OrderState.DELIVERY &&
                    order.getOrderStateId() != OrderState.COMPLETE) {   //  Если заказ сохранен (оформлен с т.з. клиента), но не оплачен (с т.з. выписанной реализации)
                addOrderInfo(order);
                a += order.getDeliverySum() + order.getSum();
            }
        }

        return a <= client.getMaxPrice();
    }

    //
    //  section Проверка обеспеченности заказа
    //

    public static class RestsError {
        private final String goodCode;

        private final String goodName;

        private final long freeCount;

        private final long reservedCount;

        private final long buzyCount;

        public RestsError(String goodCode, String goodName, long freeCount, long reservedCount, long buzyCount) {
            this.goodCode = goodCode;
            this.goodName = goodName;
            this.freeCount = freeCount;
            this.reservedCount = reservedCount;
            this.buzyCount = buzyCount;
        }

        public String getGoodCode() {
            return goodCode;
        }

        public String getGoodName() {
            return goodName;
        }

        public long getFreeCount() {
            return freeCount;
        }

        public long getReservedCount() {
            return reservedCount;
        }

        public long getBuzyCount() {
            return buzyCount;
        }
    }

    public List<RestsError> getRestsErrors() {
        Map<Long, Long> goods = new HashMap<Long, Long>();

        List<Order> orders = getOrders(null);
        for (Order order : orders) {
            if (order.getOrderStateId() != OrderState.PRE_SAVED &&
                    order.getOrderStateId() != OrderState.DELIVERY &&
                    order.getOrderStateId() != OrderState.COMPLETE) {   //  Если заказ сохранен (оформлен с т.з. клиента), но не оплачен (с т.з. выписанной реализации)
                List<OrderItem> orderItems = getOrderItemsByOrderId(order.getId());
                for (OrderItem orderItem : orderItems) {
                    long goodId = orderItem.getGoodId();
                    int count = orderItem.getCount();

                    if (!goods.containsKey(goodId)) {
                        goods.put(goodId, (long) count);
                    }
                    goods.put(goodId, goods.get(goodId) + count);
                }
            }
        }
        orders = null;

        List<RestsError> restsErrors = new ArrayList<RestsError>();
        for (long goodId : goods.keySet()) {
            Good good = getGoodById(goodId);
            if (good.getCount() < (long) goods.get(goodId)) {   //  Если товара на складе меньше, чем в заказах
                restsErrors.add(new RestsError(good.getCode(), good.getName(), good.getCount(), good.getReservedCount(), (long) goods.get(goodId)));
            }
        }

        return restsErrors;
    }

    public void checkOrders() throws MailerException {
        List<RestsError> restsErrors = getRestsErrors();
        if (restsErrors.size() == 0) {
            return;
        }

        logger.warn("Rests errors were detected!");
        StringBuilder sb = new StringBuilder();
        sb.append("Внимание! В ходе загрузки остатков из 1С в ИС было выявлено, что товара на складе не достаточно для обеспечения заказов.");
        sendEmailMsg(get("admin.mail"), sb.toString());
    }

    //
    //  Работа с о.е.
    //

    /**
     * Получить все оригинальные номера для товара по коду
     *
     * @param goodCode код товара
     * @return оригинальные номера для товара
     */
    public List<String> getOesForGood(String goodCode) {
        _assertNotBlank(goodCode);

        return new ArrayList<String>(collect(getSession().createQuery("from GoodOe where goodCode=?")
                .setParameter(0, goodCode)
                .list(), new Transformer() {

            public Object transform(Object o) {
                return ((GoodOe) o).getOe();
            }
        }));
    }

    /**
     * Добавить в справочник оригинальных номеров номер для указанного товара
     *
     * @param goodCode код товара
     * @param oe       оригинальный номер
     */
    public void addOeForGood(String goodCode, String oe) {
        _assertNotBlank(goodCode);
        _assertNotBlank(oe);

        GoodOe goodOe = (GoodOe) getSession().createQuery("from GoodOe where oe=? order by oe").setParameter(0, oe).uniqueResult();

        if (goodOe != null) {   //  Выполняем update
            goodOe.setGoodCode(goodCode);

            Transaction tx = getSession().beginTransaction();
            try {
                getSession().update(goodOe);
                tx.commit();
            } finally {
                if (!tx.wasCommitted()) {
                    tx.rollback();
                }
            }
        } else {   //  Выполняем insert
            Transaction tx = getSession().beginTransaction();
            try {
                getSession().insert(GoodOe.create(oe, goodCode));
                tx.commit();
            } finally {
                if (!tx.wasCommitted()) {
                    tx.rollback();
                }
            }
        }
    }

    /**
     * Удалить из справочника оригинальных номеров данный номер
     *
     * @param oe оригинальный номер
     */
    public void removeOeForGood(String oe) {
        _assertNotBlank(oe);

        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createQuery("delete from GoodOe where oe=?").setParameter(0, oe).executeUpdate();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    /**
     * Удалить из справочника оригинальных номер все номера для данного код товара
     *
     * @param goodCode код товара
     */
    public void clearOeForGood(String goodCode) {
        _assertNotBlank(goodCode);

        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createQuery("delete from GoodOe where goodCode=?").setParameter(0, goodCode).executeUpdate();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    //
    //  Рассылка остатков для клиентов
    //

    public List<RestsExport> getRestsExports() {
        return getSession().createQuery("from RestsExport").list();
    }

    public RestsExport getRestExportByClientId(long clientId) {
        return (RestsExport) getSession().get(RestsExport.class, clientId);
    }

    public void saveRestsExport(RestsExport restsExport) {
        RestsExport re = (RestsExport) getSession().get(RestsExport.class, restsExport.getClientId());
        if (re == null) {   //  insert
            Transaction tx = getSession().beginTransaction();
            try {
                getSession().insert(restsExport);
                tx.commit();
            } finally {
                if (!tx.wasCommitted()) {
                    tx.rollback();
                }
            }
        } else {   //  update
            Transaction tx = getSession().beginTransaction();
            try {
                getSession().update(restsExport);
                tx.commit();
            } finally {
                if (!tx.wasCommitted()) {
                    tx.rollback();
                }
            }
        }
    }

    public void removeRestsExport(long clientId) {
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createQuery("delete from RestsExport where clientId = ?").setLong(0, clientId).executeUpdate();
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    //  --------------------------------------------------------------

    //
    //  Брэнды -------------------------------------------------------
    //

    /**
     * Получить перечень идентификаторов видимых брэндов
     */
    public Set<Long> getVisibleBrands() {
        Transaction tx = null;
        List<BigInteger> l = null;
        try {
            tx = getSession().beginTransaction();
            l = getSession().createSQLQuery("select b.`Id` as `Id` from `Brands` as b where b.`IsActive` = 1").list();
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }

        Set<Long> result = new HashSet<Long>();
        for (BigInteger bigInteger : l) {
            result.add(bigInteger.longValue());
        }
        return result;
    }

    /**
     * Получить перечень идентификаторов видимых ДЛЯ КЛИЕНТА брэндов
     */
    public Set<Long> getVisibleBrandsForClient(long clientId) {
        Transaction tx = null;
        List<BigInteger> l = null;
        try {
            tx = getSession().beginTransaction();
            l = getSession().createSQLQuery("select c.`IdBrand` as `Id` from `ConClientsBrandsVisible` as c where c.`IdClient` = :clientId union select b.`Id` as `Id` from `Brands` as b where b.`IsActive` = 1")
                    .setParameter("clientId", clientId)
                    .list();
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
        Set<Long> result = new HashSet<Long>();
        for (BigInteger bigInteger : l) {
            result.add(bigInteger.longValue());
        }
        return result;
    }

    /**
     * Проверить, что указанный брэнд является видимым
     *
     * @param brandId идентификатор брэнда
     */
    public boolean getBrandIsVisible(long brandId) {
        return getVisibleBrands().contains(brandId);
    }

    /**
     * Проверить, что указанный брэнд является видимым ДЛЯ КЛИЕНТА
     *
     * @param brandId  идентификатор брэнда
     * @param clientId идентификатор клиента
     */
    public boolean getBrandIsVisibleForClient(long clientId, long brandId) {
        return getVisibleBrandsForClient(clientId).contains(brandId);
    }

    public List<Brand> getBrands() {
        int isShowNotactiveBrandsOn = getIntOrCreateIfNotExists("show.notactive.brands", 0, "Показывать ли неактивные бренды, их колонки и продукцию.");
        if(isShowNotactiveBrandsOn == 0){
            return (List<Brand>) getSession().createQuery("from Brand where isActive = 1").list();
        }
        else {
            return (List<Brand>) getSession().createQuery("from Brand").list();
        }
    }

    public Brand getBrandById(long id) {
        return (Brand) get(Brand.class, id);
    }

    public void saveBrand(Brand brand) {
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().insert(brand);
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    public void updateBrand(Brand brand) {
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().update(brand);
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    public void removeBrand(long id) {
        Transaction tx = getSession().beginTransaction();
        try {
            //  Удаляем брэнд
            getSession().createQuery("delete from Brand where id = ?").setLong(0, id).executeUpdate();

            //  Удаляем связанные группы товаров
            getSession().createQuery("update GoodType set brandId = ? where brandId = ?")
                    .setLong(0, 0)
                    .setLong(1, id)
                    .executeUpdate();

            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    public void saveGoodTypeBrand(long goodTypeId, Long brandId) {
        List<GoodType> goodTypes = getGoodTypesRecursivelyByParentId(goodTypeId, true);
        Transaction tx = getSession().beginTransaction();
        try {
            for (GoodType goodType : goodTypes) {
                getSession().createQuery("update GoodType set brandId = ? where id = ?")
                        .setParameter(0, brandId)
                        .setLong(1, goodType.getId())
                        .executeUpdate();
            }
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    //  --------------------------------------------------------------

    //
    //  Расширенная информация о доставке ----------------------------
    //

    private ClientDeliveryInfoExtender clientsDeliveryInfo = new ClientDeliveryInfoExtender(this);

    public ClientDeliveryInfoExtender getClientsDeliveryInfo() {
        return clientsDeliveryInfo;
    }

    public void setClientsDeliveryInfo(ClientDeliveryInfoExtender clientsDeliveryInfo) {
        this.clientsDeliveryInfo = clientsDeliveryInfo;
    }
    //  --------------------------------------------------------------

    private ConnGoodsBrandsCacheExtender connGoodsBrandsCache = new ConnGoodsBrandsCacheExtender(this);

    public static void main(String[] args) throws Exception {
        System.out.println(Goodfil.generatePassword());
    }


    /**
     * Выставить цены всех товаров в 0
     */
    public void clearPricelist() {
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createSQLQuery("update `SimplePricelist` set `Price` = 0").executeUpdate();
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    /**
     * Объединение заявок
     *
     * @param orderId         текущий заказ
     * @param orderForCombine заказ, выбранный для объединения
     */
    public synchronized void combineOrders(long orderId, long orderForCombineId) throws ValidationException {
        Order order = getOrderById(orderId);
        Order orderForCombine = getOrderById(orderForCombineId);
        if(orderForCombine.getC1Num() != null && order.getC1Num() == null){
            order.setC1Num(orderForCombine.getC1Num());
            updateOrder(order);
        }
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().createSQLQuery("call combine(?,?)").setParameter(0, orderId).setParameter(1, orderForCombineId).executeUpdate();
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    /*
    ======================================================================================================================================================================
    Продажи. В перспективе возможен перевощд на хранимые процедуры.
     */
    public BigInteger getGoodSalesForMonth(Good good) {
        BigInteger sales = (BigInteger) getSession().createSQLQuery("SELECT CAST(SUM(o1.Count) AS SIGNED) FROM orders o \n" +
                " JOIN orderitems o1 ON o.Id = o1.IdOrder\n" +
                "JOIN goods g ON o1.IdGood = g.Id\n" +
                "WHERE g.Id = ? AND o.IdOrderState IN (13,12,11,10,9,8,7,6,5,4,3,2) AND MONTH(o.DeliveryDate) = MONTH(NOW()) AND YEAR(o.DeliveryDate) = YEAR(NOW())\n" +
                "GROUP BY g.Id;").setParameter(0, good.getId()).uniqueResult();
        if (sales == null) {
            sales = BigInteger.valueOf(0);
        }
        return sales;
    }

    public BigInteger getGoodSalesForThreeMonths(Good good) {
        BigInteger sales = (BigInteger) getSession().createSQLQuery("SELECT CAST(SUM(o1.Count) AS SIGNED) FROM orders o \n" +
                "  JOIN orderitems o1 ON o.Id = o1.IdOrder\n" +
                "  JOIN goods g ON o1.IdGood = g.Id\n" +
                "  WHERE g.Id = ? AND o.IdOrderState IN (13,12,11,10,9,8,7,6,5,4,3,2) AND o.DeliveryDate >= (NOW() - INTERVAL 3 MONTH)\n" +
                "  GROUP BY g.Id;").setParameter(0, good.getId()).uniqueResult();
        if (sales == null) {
            sales = BigInteger.valueOf(0);
        }
        return sales;
    }

    public BigInteger getGoodSalesForSixMonths(Good good) {
        BigInteger sales = (BigInteger) getSession().createSQLQuery("SELECT CAST(SUM(o1.Count) AS SIGNED) FROM orders o \n" +
                "  JOIN orderitems o1 ON o.Id = o1.IdOrder\n" +
                "  JOIN goods g ON o1.IdGood = g.Id\n" +
                "  WHERE g.Id = ? AND o.IdOrderState IN (13,12,11,10,9,8,7,6,5,4,3,2) AND o.DeliveryDate >= (NOW() - INTERVAL 6 MONTH)\n" +
                "  GROUP BY g.Id").setParameter(0, good.getId()).uniqueResult();
        if (sales == null) {
            sales = BigInteger.valueOf(0);
        }
        return sales;
    }

    public BigInteger getGoodSalesForYear(Good good) {
        BigInteger sales = (BigInteger) getSession().createSQLQuery("SELECT CAST(SUM(o1.Count) AS SIGNED) FROM orders o \n" +
                "  JOIN orderitems o1 ON o.Id = o1.IdOrder\n" +
                "  JOIN goods g ON o1.IdGood = g.Id\n" +
                "  WHERE g.Id = ? AND o.IdOrderState IN (13,12,11,10,9,8,7,6,5,4,3,2) AND o.DeliveryDate >= (NOW() - INTERVAL 12 MONTH)\n" +
                "  GROUP BY g.Id").setParameter(0, good.getId()).uniqueResult();
        if (sales == null) {
            sales = BigInteger.valueOf(0);
        }
        return sales;
    }

    /*
    =====================================================================================================================================================================
    Работа со статисктикой по недосдачам
     */
    public void saveGoodAbsence(GoodAbsence goodAbsence) {
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().insert(goodAbsence);
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    public void saveGoodAbsences(List<Map<String, String>> list, long clientId) {
        for (Map<String, String> map : list) {
            saveGoodAbsence(new GoodAbsence(
                    getClientById(clientId).getManagerId(),
                    getGoodByCode(map.get("code")).getId(),
                    clientId,
                    Integer.parseInt(map.get("requestedCount")) - Integer.parseInt(map.get("count")),
                    Double.parseDouble(map.get("price")))
            );
        }
    }

    public List<GoodAbsence> getGoodAbsence(Date beginDate, Date endDate) {
        List<GoodAbsence> result = new ArrayList<GoodAbsence>();
        List list = getSession().createSQLQuery("SELECT g.Id, g.IdManager, g.IdGood, g.IdClient, SUM(g.GoodCount), g.Date, Count(Distinct g.Id) FROM goodabsences g\n" +
                " WHERE g.Date BETWEEN ? AND ? GROUP BY g.IdGood").setParameter(0, beginDate).setParameter(1, endDate).list();
        for (Object o : list) {
            Object[] arr = (Object[]) o;
            try {
                long id = ((BigInteger) arr[0]).longValue();
                long idManager = ((BigInteger) arr[1]).longValue();
                long idGood = ((BigInteger) arr[2]).longValue();
                long idClient = ((BigInteger) arr[3]).longValue();
                BigDecimal count = (BigDecimal) arr[4];
                java.sql.Timestamp timestamp = (java.sql.Timestamp) arr[5];
                int ordersCount = ((BigInteger)arr[6]).intValue();

                GoodAbsence goodAbsence = new GoodAbsence();
                goodAbsence.setId(id);
                goodAbsence.setIdManager(idManager);
                goodAbsence.setIdGood(idGood);
                goodAbsence.setIdClient(idClient);
                goodAbsence.setCount(count.intValue());
                Calendar cal = Calendar.getInstance();
                cal.setTimeInMillis(timestamp.getTime());
                goodAbsence.setCalendar(cal);
                goodAbsence.setOrdersCount(ordersCount);
                result.add(goodAbsence);
            } catch (NullPointerException e) {

            }
        }
        return result;
    }

    public List getDetaildeInfo(long goodId, Date beginDate, Date endDate) {
        List list = getSession().createSQLQuery("SELECT g.IdManager, g.IdClient, COUNT(DISTINCT g.Id), SUM(g.GoodCount) FROM goodabsences g WHERE g.IdGood = ?\n" +
                " AND g.Date BETWEEN ? AND ? GROUP BY g.IdClient ORDER BY g.IdManager").setParameter(0, goodId).setParameter(1, beginDate).setParameter(2, endDate).list();
        return list;
    }

    public long getAbsenseByGoodTypeId(long id, Date beginDate, Date endDate) {
        BigDecimal result = (BigDecimal) getSession().createSQLQuery("SELECT SUM(g.GoodCount) FROM goodabsences g JOIN goods g1 ON g.IdGood = g1.Id\n" +
                " WHERE g1.IdGoodType = ? AND g.Date BETWEEN ? AND ? GROUP BY g1.IdGoodType")
                .setParameter(0, id).setParameter(1, beginDate).setParameter(2, endDate).uniqueResult();
        return result == null ? 0 : result.longValue();
    }

    public long getAbsenceCostByGoodTypeId(long id, Date beginDate, Date endDate) {
        Double result = (Double) getSession().createSQLQuery("call getGoodAbsenceCost(?, ?, ?)").setParameter(0, id).setParameter(1, beginDate).setParameter(2, endDate).uniqueResult();
        /*Double result = (Double) getSession().createSQLQuery("SELECT SUM(g.GoodCount) * s.Price FROM goodabsences g \n" +
                "  JOIN goods g1 JOIN clients c JOIN clientgroups c1 JOIN users u \n" +
                "  JOIN clientsclientgroups c2 JOIN brands b JOIN simplepricelist s\n" +
                "  ON g.IdGood = g1.Id AND g.IdClient = c.Id AND c1.IdBrand = b.Id AND c.IdManager = u.Id \n" +
                "  AND c1.Id = c2.IdClientGroup AND c.Id = c2.IdClient AND c1.Id = s.IdClientGroup AND g1.Id = s.IdGood\n" +
                "WHERE b.Id = 1 AND g1.IdGoodType = ? AND g.Date BETWEEN ? AND ? GROUP BY g1.IdGoodType").setParameter(0, id).setParameter(1, beginDate).setParameter(2, endDate).uniqueResult();*/
        return result == null ? 0 : result.longValue();
    }
/*
=============================================================================================================================================================================
Работа с листом развоза
*/

    /**
     * Возвращает список заказов для листа развоза на выбранную дату. Это:
     * 1. Неудаленные заказы
     * 2. Статус "Обработка завершена"
     * 3. Дата отгрузки либо после выбранной, либо раньше, но заказ не отгрузили
     */
    public List<StorageDeliveryStatus> getItemsForStorageDelivery(Date date) {
/* чтобы вернуться к использованию закомментированного кода нужно добавить в метод второй параметр delieryType типа int
 return (List<StorageDeliveryStatus>) getSession().createQuery("from StorageDeliveryStatus where currentDate = ? and idDeliveryType = ? and clientStateId != ?").setParameter(0, date).
        setParameter(1, deliveryType).setParameter(2, ClientState.E_SHOP_DAY_TO_DAY).list();*/
        return (List<StorageDeliveryStatus>) getSession().createQuery("from StorageDeliveryStatus where currentDate = ? ").setParameter(0, date).
                list();
    }

    public List<StorageDeliveryStatus> getItemsForStorageDeliverySelfDeliveryDaytoDay(Date date) {
        return (List<StorageDeliveryStatus>) getSession().createQuery("from StorageDeliveryStatus where currentDate = ? and idDeliveryType = ? and clientStateId = ?").
                setParameter(0, date).setParameter(1, DeliveryType.SELF_DELIVERY).setParameter(2, ClientState.E_SHOP_DAY_TO_DAY).list();
    }

    public void saveStorageDeliveryStatus(StorageDeliveryStatus storageDeliveryStatus) {
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().insert(storageDeliveryStatus);
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    public void updateStorageDeliveryStatus(Collection<? extends StorageDeliveryStatus> storageDeliveryStatuses) throws ValidationException {
        Calendar now = Calendar.getInstance();
        now.set(Calendar.HOUR_OF_DAY, 0);
        now.set(Calendar.MINUTE, 0);
        now.set(Calendar.SECOND, 0);
        now.set(Calendar.MILLISECOND, 0);
        if (now.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY) {
            now.add(Calendar.DATE, 3);
        } else if (now.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
            now.add(Calendar.DATE, 2);
        } else {
            now.add(Calendar.DATE, 1);
        }
        for (StorageDeliveryStatus status : storageDeliveryStatuses) {
            List<StorageDeliveryStatus> deliveryStatusList = getPeriodicStoragedeliveryStatusByOrderIdAndDate(status.getIdOrder(), new Date(status.getDate().getTime() + TimeUnit.DAYS.toMillis(1)));
            if (deliveryStatusList.size() == 0) {
                if (status.getDeliveryState() == StorageDeliveryState.NOT_DELIVERY) {
                    saveStorageDeliveryStatus(new StorageDeliveryStatus(status.getIdOrder(), now.getTime(), status.getIdDeliveryType(), status.getClientStateId()));
                }
            }
        }
        update(storageDeliveryStatuses);
    }

    /**
     * Лист развоза по дате и id заказа
     *
     * @param orderId
     * @param date
     * @return
     */
    private List<StorageDeliveryStatus> getPeriodicStoragedeliveryStatusByOrderIdAndDate(long orderId, Date date) {
        return getSession().createQuery("from StorageDeliveryStatus where currentDate = ? and IdOrder=?").setParameter(0, date).setParameter(1, orderId).list();
    }

    /**
     * Лист развоза по Id заказа
     *
     * @param orderId
     * @return
     */
    public List<StorageDeliveryStatus> getStorageDeliveryStatusByOrderId(long orderId) {
        return (List<StorageDeliveryStatus>) getSession().createQuery("from StorageDeliveryStatus where IdOrder = ?").setParameter(0, orderId).list();
    }

    /**
     * Несохранненные элементы более ранних листов развоза
     *
     * @param date
     * @return
     */
    public List<StorageDeliveryStatus> getOutstandingStorageDeliveryStatus(Date date) {
/*        if(deliveryType == 3){
            return (List<StorageDeliveryStatus>) getSession().createQuery("from StorageDeliveryStatus where currentDate < ? and IsSaved = 0 and idDeliveryType = ? and clientStateId = ? order by currentDate").setParameter(0, date).setParameter(1, DeliveryType.SELF_DELIVERY).setParameter(2, ClientState.E_SHOP_DAY_TO_DAY).list();
        }
        return (List<StorageDeliveryStatus>)getSession().createQuery("from StorageDeliveryStatus where currentDate < ? and IsSaved = 0 and idDeliveryType = ? " +
                "and clientStateId != ? order by currentDate").setParameter(0, date).setParameter(1, deliveryType).setParameter(2, ClientState.E_SHOP_DAY_TO_DAY).list();*/
        return (List<StorageDeliveryStatus>) getSession().createQuery("from StorageDeliveryStatus where currentDate < ? and IsSaved = 0 " +
                "order by currentDate").setParameter(0, date).list();
    }

    public List<User> getStoreKeepers() {
        return (List<User>) getSession().createQuery("from User where userRoleId = 3").list();
    }

    public void removeStorageDelviveryStatus(long id) throws RemoveFailureException, InvalidIdException {
        remove(StorageDeliveryStatus.class, id);
    }

    public StorageDeliveryStatus getStorageDeliveryStatusById(long id) {
        return (StorageDeliveryStatus) getSession().createQuery("from StorageDeliveryStatus where id = ?").setParameter(0, id).uniqueResult();
    }

    /*
    =======================================================================================================================================================================
    Дополнения в складской модуль
     */
    public synchronized List<OrderItem> getOrderItemsSumByOrderIds(ArrayList<Long> orderIds) {
        List<OrderItem> result = new ArrayList<OrderItem>();
        List list = getSession().createSQLQuery("SELECT o.IdGood, o.IdOrder, SUM(o.Count)\n" +
                "  FROM orderitems o WHERE o.IdOrder IN (:orderIds)  GROUP BY o.IdGood").setParameterList("orderIds", orderIds).list();
        for (Object o : list) {
            Object[] arr = (Object[]) o;
            try {

                long idGood = ((BigInteger) arr[0]).longValue();
                long idOrder = ((BigInteger) arr[1]).longValue();
                Integer count = ((BigDecimal) arr[2]).intValue();


                OrderItem item = new OrderItem();
                item.setGoodId(idGood);
                item.setGood(getGoodById(idGood));
                item.setOrderId(idOrder);
                item.setCount(count);
                result.add(item);
            } catch (NullPointerException e) {

            }
        }
        return result;
    }


    /*==============================================================================================================================*/
    public List<OrderItem> getOrderItemsFactGivenStores() {

        List<OrderItem> result = new ArrayList<OrderItem>();
        List list = (List<OrderItem>) getSession().createSQLQuery("SELECT o.IdGood, o.IdOrder, SUM(o.Count) FROM orderitems o \n" +
                "  WHERE o.FactGiverFromStore = 0 GROUP BY o.IdGood").list();
        for (Object o : list) {
            Object[] arr = (Object[]) o;
            try {

                long idGood = ((BigInteger) arr[0]).longValue();
                long idOrder = ((BigInteger) arr[1]).longValue();
                Integer count = ((BigDecimal) arr[2]).intValue();


                OrderItem item = new OrderItem();
                item.setGoodId(idGood);
                item.setGood(getGoodById(idGood));
                item.setOrderId(idOrder);
                item.setCount(count);
                result.add(item);
            } catch (NullPointerException e) {

            }
        }
        return result;
    }

    public List<Driver> getDrivers() {
        return getSession().createQuery("from Driver").list();
    }

    public Driver getDriverById(long id) {
        return (Driver) getSession().createQuery("from Driver d where d.id = ?").setParameter(0, id).uniqueResult();
    }

    /*============================================================================================================================*/

    public List getAbsenceByClient(Date beginDate, Date endDate, long clientId) {
        return getSession().createSQLQuery("call clientAbsence(?,?,?)").setParameter(0, clientId).setParameter(1, beginDate).setParameter(2, endDate).list();
    }

    public List getDeliveriesByClient(Date beginDate, Date endDate, long clientId) {
        return getSession().createSQLQuery("call clientDeliveries(?,?,?)").setParameter(0, clientId).setParameter(1, beginDate).setParameter(2, endDate).list();
    }

    public List getAbsenceByManager(Date beginDate, Date endDate, long managerId) {
        return getSession().createSQLQuery("call allAbsenceByManager(?,?,?)").setParameter(0, managerId).setParameter(1, beginDate).setParameter(2, endDate).list();
    }

    public List getDeliveriesByManager(Date beginDate, Date endDate, long managerId) {
        return getSession().createSQLQuery("call allDeliveriesByManager(?,?,?)").setParameter(0, managerId).setParameter(1, beginDate).setParameter(2, endDate).list();
    }

    public List getStorekeeperAbsence(Date beginDate, Date endDate) {
        return getSession().createSQLQuery("call storekeeperAbsence(?,?)").setParameter(0, beginDate).setParameter(1, endDate).list();
    }

    public List<Order> getDeliveryOrdersByDate(Date date) {
        return getSession().createQuery("from Order o where DATE(o.factDeliveryDate) = ?").setParameter(0, date).list();
    }

    // Старый фильтр заказов нифига нормально не работает, также нужно многое учесть. Поэтому решил написать свою выгрузку заказов для формирования отчета склада.
    public List<Order> getOrdersByDeliveryDate(Date beginDate, Date endDate) {
        List<Order> result = getSession().createQuery("from Order where deliveryDate between ? and ? and idOrderState in (8,9,11) and deliveryDate != null").
                setParameter(0, beginDate).setParameter(1, endDate).list();
        for (Order order : result) {
            addOrderInfo(order);
            order.setClient(getClientById(order.getClientId()));
            order.setManager(getUserById(order.getManagerId()));
            setOrderExpectedAssemblyDate(order);
        }
        return result;
    }

    public void saveStorageEnterance(StorageEnterance storageEnterance) {
        Transaction tx = getSession().beginTransaction();
        try {
            getSession().insert(storageEnterance);
            tx.commit();
        } finally {
            if (!tx.wasCommitted()) {
                tx.rollback();
            }
        }
    }

    /**
     * Метод для изменения количества товара при отправке заказа в 1С
     * Алгоритм:
     * 1. Отнять количество товара в заказе от общего количества товата
     * 2. Отнять количество товара в заказе от зарезервированного кол-ва товара
     * 3. Отнять количество товара в закаке от остатков на складе (для контроля)
     * Первые два пункта выполнять только если включено резервирование на стороне ИС
     * @param orderItems
     */
    public void debitOrderItems(long orderId, int orderState) throws java.sql.BatchUpdateException, GoodfilException {

        if(orderState != OrderState.DELIVERY) throw new GoodfilException("Неверый статус заказа");

        List<OrderItem> orderItems = getOrderItemsByOrderId(orderId);
        for(OrderItem item : orderItems){
            Good good = getGoodById(item.getGoodId());
            if(good.getReservedCount() >= item.getCount() && good.getCount() >= item.getFactGivenFromStore()){
                good.setReservedCount(good.getReservedCount() - item.getCount());
                good.setCount(good.getCount() - item.getFactGivenFromStore());
                good.setStorageCount(good.getStorageCount() - item.getFactGivenFromStore());
                updateGood(good);
            }
            else {
                throw new GoodfilException("Ошибка обновления данных. Свяжитесь с администратором.");
            }
        }
    }

    /**
     * Метод для изменения количества товара при удалении заказа
     * Если заказ не проведен в 1С - снять позиции заказа с резерва
     * Если заказ проведен, то обновить количество (в принципе придет из 1С, надо посчитать быстродействие) и остатки на складе
     * @param orderId идентификатор заказа
     * @param orderState статус заказа
     */
    public void updateGoodCountByDeletingOrder(long orderId, int orderState) throws IllegalArgumentException, ValidationException {
        List<OrderItem> orderItems = getOrderItemsByOrderId(orderId);
        List<Good> goods = new ArrayList<Good>();
        for(OrderItem orderItem : orderItems){
            Good good = getGoodById(orderItem.getGoodId());
                // Если статус заказа "На складе: отгрузка" или "Обработка завершена"
                if(orderState == OrderState.DELIVERY || orderState == OrderState.COMPLETE){
                    // Добавляем позиции заказа к количеству товара
                    good.setCount(good.getCount() + orderItem.getFactGivenFromStore());
                    // В складские остатки
                    good.setStorageCount(good.getStorageCount() + orderItem.getFactGivenFromStore());
                }
                else if(orderState != OrderState.PRE_SAVED){
                    // Если статус заказа "Резерв", "На складе: сборка" или "Принят в обработку складом"
                    int goodReservedCount = good.getReservedCount();
                    if(goodReservedCount >= orderItem.getCount()){
                        // Снимаем товар с резерва
                        good.setReservedCount(goodReservedCount - orderItem.getCount());
                    }
                    else {
                        throw new IllegalArgumentException("Ошибка обновления данных для товара с артикулом " + good.getCode() + ".");
                    }
                }
            //updateGood(good);
            goods.add(good);
        }
        updateGoods(goods);
    }

    public boolean getIsReservedOn(){
        int isReserved = getIntOrCreateIfNotExists("is.reserves.on", 0, "Включить резервирование на стороне ИС");
        return isReserved == 1;
    }

    public void getOrderCalculation(long orderId){
        DataExchange dataExchange = new DataExchange();
        dataExchange.setItemId(orderId);
        dataExchange.setTableName("OrderPrint");
        Transaction tx = getSession().beginTransaction();
        getSession().insert(dataExchange);
        tx.commit();
    }

    public double getVolumeOfExportedGoods(Date beginDate, Date endDate){
        Double sum = (Double)getSession().createSQLQuery("call volumeOfExportedGoods(?,?)").setParameter(0, beginDate).setParameter(1, endDate).uniqueResult();
        if (sum == null) sum = 0.0;
        return sum;
    }

    public StorageReturn getStorageReturnById(long id){
        return (StorageReturn) getSession().createQuery("from StorageReturn where id = ?").setParameter(0, id).uniqueResult();
    }

    public void updateStorageReturn(StorageReturn storageReturn) throws ValidationException {
        update(storageReturn);
    }

    public List getTopFiftyProducts(){
        return getSession().createSQLQuery("call topProducts").list();
    }
}